diff --git a/.gitignore b/.gitignore index 80b79d73..75a103c6 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ ENV/ .mypy_cache/ *.root *.png + +# CMake build directory +build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 17ca821f..78cf125b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,13 +9,13 @@ include( PackageBuilder ) pbuilder_prepare_project() +set_option( Scarab_BUILD_CODEC_JSON OFF ) +set_option( Scarab_BUILD_CODEC_YAML OFF ) +set_option( Scarab_BUILD_AUTHENTICATION OFF ) +set_option( Scarab_BUILD_CLI OFF ) +set_option( Scarab_BUILD_PARAM OFF ) +set_option( Scarab_ENABLE_EXECUTABLES OFF ) +set_option( Cicada_ENABLE_KATYDID_NAMESPACE OFF ) + pbuilder_add_submodule( Cicada Cicada ) pbuilder_add_submodule( Phylloxera Phylloxera ) - -set( Scarab_BUILD_CODEC_JSON OFF CACHE BOOL "No coded" FORCE) -set( Scarab_BUILD_CODEC_YAML OFF CACHE BOOL "No codec" FORCE) -set( Scarab_BUILD_AUTHENTICATION OFF CACHE BOOL "No auth" FORCE) -set( Scarab_BUILD_PARAM OFF CACHE BOOL "No param" FORCE) - -set( Cicada_ENABLE_KATYDID_NAMESPACE OFF CACHE BOOL "Use Cicada namespace" FORCE) - diff --git a/Cicada b/Cicada index eac4c64a..66779bac 160000 --- a/Cicada +++ b/Cicada @@ -1 +1 @@ -Subproject commit eac4c64adbcd6412912599ee754a953d01b4a860 +Subproject commit 66779bacb7cab84fa75a103a5ef7087bb87e960a diff --git a/Dockerfile b/Dockerfile index 2c7fb38e..3599cffa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,6 @@ RUN mkdir -p $MERMITHID_BUILD_PREFIX &&\ echo 'export PYTHONPATH=$MERMITHID_BUILD_PREFIX/$(python3 -m site --user-site | sed "s%$(python3 -m site --user-base)%%"):$PYTHONPATH' >> setup.sh &&\ /bin/true -RUN source $COMMON_BUILD_PREFIX/setup.sh &&\ - pip install iminuit &&\ - /bin/true - ######################## FROM mermithid_common as mermithid_done @@ -45,6 +41,7 @@ COPY tests $MERMITHID_BUILD_PREFIX/tests # repeat the cmake command to get the change of install prefix to set correctly (a package_builder known issue) RUN source $MERMITHID_BUILD_PREFIX/setup.sh &&\ +# pip3 install --upgrade pip &&\ cd /tmp_source &&\ mkdir -p build &&\ cd build &&\ @@ -64,3 +61,4 @@ RUN source $MERMITHID_BUILD_PREFIX/setup.sh &&\ FROM mermithid_common COPY --from=mermithid_done $MERMITHID_BUILD_PREFIX $MERMITHID_BUILD_PREFIX +RUN pip3 install seaborn statsmodels diff --git a/Phylloxera b/Phylloxera index 5d2ce40b..908f83c4 160000 --- a/Phylloxera +++ b/Phylloxera @@ -1 +1 @@ -Subproject commit 5d2ce40b3e7fd65d17c5506169903ecb0cb94c63 +Subproject commit 908f83c4cccc7c1a02d4afe87a4742f29dfbec1d diff --git a/mermithid/misc/ComplexLineShapeUtilities.py b/mermithid/misc/ComplexLineShapeUtilities.py index 4fe05dc7..4566ac58 100644 --- a/mermithid/misc/ComplexLineShapeUtilities.py +++ b/mermithid/misc/ComplexLineShapeUtilities.py @@ -39,9 +39,12 @@ def read_oscillator_str_file(filename): for line in lines: if line != "" and line[0]!="#": - raw_data = [float(i) for i in line.split("\t")] - energyOsc[0].append(raw_data[0]) - energyOsc[1].append(raw_data[1]) + try: + raw_data = [float(i) for i in line.split("\t")] + energyOsc[0].append(raw_data[0]) + energyOsc[1].append(raw_data[1]) + except: + continue energyOsc = np.array(energyOsc) ### take data and sort by energy @@ -50,9 +53,14 @@ def read_oscillator_str_file(filename): energyOsc[1] = energyOsc[1][sorted_indices] return energyOsc -# A sub function for the scatter function. Found in +# A sub function for the scatter function. The oscillator strength tails are from +# this database: https://nl.lxcat.net/home/. +# To get the parameters below, the tails are then fitted using the function in: # "Energy loss of 18 keV electrons in gaseous T and quench condensed D films" # by V.N. Aseev et al. 2000 +# The tails in the LXCAT database sometimes only go out to a certain energy. +# Sometimes, we need energies extending beyond that value. That is why we +# need to use this parameterized form of the tail. def aseev_func_tail(energy_loss_array, gas_type): if gas_type=="H2": A2, omeg2, eps2 = 0.195, 14.13, 10.60 @@ -62,14 +70,21 @@ def aseev_func_tail(energy_loss_array, gas_type): A2, omeg2, eps2 = 0.1187, 33.40, 10.43 elif gas_type=="Ar": A2, omeg2, eps2 = 0.3344, 21.91, 21.14 + elif gas_type=="N2": + A2, omeg2, eps2 = 0.21754816, 44.99897054, 20.43916114 + elif gas_type=="CO": + A2, omeg2, eps2 = 0.19583454, 55.21888452, 16.44972596 + elif gas_type=="C2H4": + A2, omeg2, eps2 = 0.57492182, 23.77501391, 14.33107345 return A2*omeg2**2./(omeg2**2.+4*(energy_loss_array-eps2)**2.) #convert oscillator strength into energy loss spectrum def get_eloss_spec(e_loss, oscillator_strength, kr_17keV_line): #energies in eV - kinetic_en = kr_17keV_line * 1000 + kinetic_en = kr_17keV_line e_rydberg = 13.605693009 #rydberg energy (eV) a0 = 5.291772e-11 #bohr radius - return np.where(e_loss>0 , 4.*np.pi*a0**2 * e_rydberg / (kinetic_en * e_loss) * oscillator_strength * np.log(4. * kinetic_en * e_loss / (e_rydberg**3.) ), 0) + argument_of_log = np.where(e_loss > 0, 4. * kinetic_en * e_rydberg / (e_loss**2.) , 1e-5) + return np.where(e_loss>0 , 1./(e_loss) * oscillator_strength* np.log(argument_of_log), 0) # Takes only the nonzero bins of a histogram def get_only_nonzero_bins(bins,hist): @@ -109,11 +124,10 @@ def energy_guess_to_frequency(energy_guess, energy_guess_err, B_field_guess): return frequency , frequency_err # Given a frequency and error, converts those to B field values assuming the line is the 17.8 keV line -def central_frequency_to_B_field(central_freq,central_freq_err): +def central_frequency_to_B_field(central_freq): const = (2.*np.pi*m_e)*(1+kr_17keV_line/mass_energy_electron)/e_charge B_field = const*central_freq - B_field_err = const*central_freq_err - return B_field , B_field_err + return B_field # given a FWHM for the lorentian component and the FWHM for the gaussian component, # this function estimates the FWHM of the resulting voigt distribution @@ -200,4 +214,4 @@ def shake_spectrum(self): x_array = flip_array(x_array) shake_spectrum = self.full_shake_spectrum(x_array, 0, 24) return shake_spectrum - ############################################################################### \ No newline at end of file + ############################################################################### diff --git a/mermithid/misc/Constants.py b/mermithid/misc/Constants.py index a967fae4..5705b850 100644 --- a/mermithid/misc/Constants.py +++ b/mermithid/misc/Constants.py @@ -32,8 +32,8 @@ def GF(): return 1.1663787*10**(-23) #Gf/(hc)^3, in eV^(-2) def Vud(): return 0.97425 #CKM element #Beta decay-specific physical constants -def QT(): return 18563.251 #For atomic tritium (eV), from Bodine et al. (2015) -def QT2(): return 18573.24 #For molecular tritium (eV), Bodine et al. (2015) +def QT(): return 18563.251 #SHOULD BE DOUBLE-CHECKED. For atomic tritium (eV), from Bodine et al. (2015) +def QT2(): return 18574.01 #For molecular tritium (eV). Calculation here: https://projecteight.slack.com/archives/CG5TY2UE7/p1649963449399179, based on Bodine et al. (2015). def Rn(): return 2.8840*10**(-3) #Helium-3 nuclear radius in units of me, from Kleesiek et al. (2018): https://arxiv.org/pdf/1806.00369.pdf def M_3He_in_me(): return 5497.885 #Helium-3 mass in units of me, Kleesiek et al. (2018) def atomic_num(): return 2. #For helium-3 diff --git a/mermithid/misc/DetectionEfficiencyUtilities.py b/mermithid/misc/DetectionEfficiencyUtilities.py new file mode 100755 index 00000000..7c04e84d --- /dev/null +++ b/mermithid/misc/DetectionEfficiencyUtilities.py @@ -0,0 +1,431 @@ + + +import numpy as np +import matplotlib.pyplot as plt +import os +from scipy.interpolate import interp1d +from scipy.stats import binom +from scipy import constants + +from scipy import constants +import scipy.special as scs + +#import moviepy.editor as mpy +c = 299792458 +m_kg = 9.10938291*1e-31 +me_keV = 510.998 +q_C = 1.60217657*1e-19 +Z0 = 119.917 * np.pi +Rc = 0.06e-2 +L0 = 0.35 +L0 = 0.3 +L0 = 0.2 +H = 0.9359 +H = 0.95777194923080811 +H = 0.9578170819250281 +E = 17.83 +a = 1.07e-2 +b = 0.43e-2 +r = 1.006e-2/2 #Circ WG Radius +theta = np.arange( 88 * np.pi / 180 , 89.999 * np.pi / 180 , 0.0001) +#theta = 89.9*np.pi/180 + +# ?? +n = 0 + + + +def binomial_interval(n, k, alpha=1 / 3.1514872): + from statsmodels.stats.proportion import proportion_confint + + if isinstance(n, list) or type(n) is np.ndarray: + + mean = np.zeros(len(n)) + interval = np.zeros((2, len(n))) + interval_mean = np.zeros(len(n)) + for i in range(len(n)): + interval[0][i], interval[1][i] = proportion_confint(k[i], n[i], method='wilson', alpha=alpha) + rv = binom(n[i], k[i]*1./n[i]) + mean[i] = rv.mean()*1./n[i] + interval_mean[i] = np.mean(interval.T[i]) + interval_error = [interval_mean - interval[0], interval[1]-interval_mean] + + else: + l_int, u_int = proportion_confint(k, n, method='wilson', alpha=alpha) + rv = binom(n, k*1./n) + mean = rv.mean()*1./n + interval_mean = np.mean([l_int, u_int]) + interval_error = [interval_mean - l_int, u_int-interval_mean] + return mean, interval_mean, interval_error + + +def freq2en(f, mixfreq=24.5e9): + """ + convert IF frequency in MHz to energy in keV + """ + B=0.9578170819250281 + f = f*1e6 + emass = constants.electron_mass/constants.e*constants.c**2 + gamma = (constants.e*B)/(2.0*np.pi*constants.electron_mass) * 1/(f+mixfreq) + + return (gamma -1)*emass*1e-3 + + +def en2freq(E, Theta=None, mixfreq=24.5e9): + """ + convert energy in keV to IF frequency in MHz + """ + B=0.9578170819250281 + E = E*1e3 + if Theta==None: + Theta=np.pi/2 + e = constants.e + c = constants.c + + emass = constants.electron_mass/constants.e*constants.c**2 + gamma = E/(emass)+1 + + return ((constants.e*B)/(2.0*np.pi*constants.electron_mass) * 1/gamma)*1e-6-mixfreq*1e-6 + + +def power_efficiency(f, plot=False, savepath='.'): + """ + Toy model efficiency using pheno paper power vs. energy and arbitrary threshold + """ + #print('Toy model efficiency') + threshold = 8 + N = int(1e6) + efficiency = np.zeros(len(f)) + efficiency_error = np.zeros((len(f),2)) + power_factor = Power(f)/Power([1407e6+24.5e9]) + + #if plot: + # N = int(1e6) + + data0 = np.random.exponential(2, N) + + if plot: + plt.figure(figsize=(7,5)) + plt.hist(data0, histtype='step', bins=100, color='blue') + plt.axvline(threshold, label='Detection threshold', color='red') + plt.ylabel('N') + plt.legend() + plt.xlabel('SNR') + plt.yscale('log') + plt.tight_layout() + plt.savefig(os.path.join(savepath, 'hypothetical_detection.png'), dpi=200, transparent=True) + plt.savefig(os.path.join(savepath, 'hypothetical_detection.pdf'), dpi=200, transparent=True) + + for i in range(len(f)): + + data = data0*power_factor[i] + efficiency[i], _, efficiency_error[i] = binomial_interval(len(data), len(data[data>threshold])) + + + if plot: + plt.figure(figsize=(7,5)) + plt.plot(freq2en(f*1e-6-24.5e3), efficiency, color=pm.sns_color[0]) + #plt.fill_between(freq2en(f*1e-6-24.5e3), efficiency-efficiency_error.T[0], efficiency+efficiency_error.T[1], color=pm.sns_color[0], alpha=0.5) + plt.ylabel('Efficiency') + plt.xlabel('Energy [keV]') + plt.tight_layout() + + #print((efficiency[-1]-efficiency[0])/(freq2en(f*1e-6-24.5e3)[-1]-freq2en(f*1e-6-24.5e3)[0])/efficiency[0]) + plt.savefig(os.path.join(savepath, 'hypothetical_efficiency.png'), dpi=200, transparent=True) + plt.savefig(os.path.join(savepath, 'hypothetical_efficiency.pdf'), dpi=200, transparent=True) + + return efficiency/np.mean(efficiency), efficiency_error.T/np.mean(efficiency) + +def interpolated_efficiency(f, snr_efficiency_dict, alpha=1): + """ + turn efficiencies at fixed frequencies into callable efficiency for arbitrary frequency + alpha scales uncertainty + """ + + # only use good index (where fits in efficiency analysis didn't fail) + index_0 = snr_efficiency_dict['good_fit_index'] + + x = np.array(snr_efficiency_dict['frequency'])[index_0] + y = np.array(snr_efficiency_dict['tritium_rates'])[index_0] + y = [y for _,y in sorted(zip(x,y))] + y_err_0 = np.array(snr_efficiency_dict['tritium_rates_error'][0])[index_0]*alpha + y_err_1 = np.array(snr_efficiency_dict['tritium_rates_error'][1])[index_0]*alpha + y_err_0 = [y_err for _,y_err in sorted(zip(x,y_err_0))] + y_err_1 = [y_err for _,y_err in sorted(zip(x,y_err_1))] + x = sorted(x) + + yp = np.interp(f, x, y, left=0, right=0) + yp_error_0 = np.interp(f, x, y_err_0, left=1, right=1) + yp_error_1 = np.interp(f, x, y_err_1, left=1, right=1) + + + return np.array(yp), np.array([yp_error_0, yp_error_1]) + +def pseudo_interpolated_efficiency(f, snr_efficiency_dict, alpha=1): + """ + gaussian random efficiency in uncertainty intervall + """ + if isinstance(f, float): + y, y_error = interpolated_efficiency(f, snr_efficiency_dict,alpha) + pseudo_y = np.random.randn()*y_error + y + else: + y, y_error = interpolated_efficiency(f, snr_efficiency_dict, alpha) + + pseudo_y = np.random.randn(len(f)) + pseudo_y[pseudo_y<0]*=np.array(y_error)[0][pseudo_y<0] + pseudo_y[pseudo_y>0]*=np.array(y_error)[1][pseudo_y>0] + + + return pseudo_y+y, y_error + + +def integrated_efficiency(f, snr_efficiency_dict, df=None , mix_freq=24.5e9, centers = True): + """ + pseudo integrates interpolated efficiency over bin width. + integral is approximated by just averaging a few points over the bin, because its much faster + + f: frequency bins in absolute frequencies (>25GHz), units is Hz + df: width around bin centers that should be integrated over. Only used if f is float + mix_freq: required because efficiency analysis was done in IF frequencies + center: if true assume f is bin centers, if False assume f is bin edges + """ + + + # number of points summed in "integration" + N = 10 + + # inteprolate efficiency and efficiency uncertainty + + frequency = np.array(snr_efficiency_dict['frequency'])[snr_efficiency_dict['good_fit_index']] + efficiency, efficiency_error = interpolated_efficiency(frequency, snr_efficiency_dict) + + interp_eff = interp1d(frequency-24.5e9, efficiency, fill_value=0, bounds_error=False) + interp_eff_error_up = interp1d(frequency-24.5e9, efficiency_error[1], fill_value=1, bounds_error=False) + interp_eff_error_down = interp1d(frequency-24.5e9, efficiency_error[0], fill_value=1, bounds_error=False) + + + # bin width + if isinstance(f, float): + if df==None: + raise Exception('No integration width given') + + + # if df is smaller than FSS bin width, do not integrate but just return interpolation + + if df > 2e6: + x = np.linspace(f-mix_freq-df/2, f-mix_freq+df/2, N) + eff = np.sum(interp_eff(x))/N + eff_err_down = np.sum(interp_eff_error_down(x))/N + eff_err_up = np.sum(interp_eff_error_up(x))/N + return eff, [eff_err_down, eff_err_down] + + else: + eff = interp_eff(f-mix_freq) + eff_error_down = interp_eff_error_down(f-mix_freq) + eff_error_up = interp_eff_error_up(f-mix_freq) + + else: # f ist list of array + df = f[1]-f[0] + + # if df is smaller than FSS bin width, do not integrate but just return interpolation + if df < 2e6: + if centers: + x = f - mix_freq + else: + x = f[0:-1]+0.5*df - mix_freq + eff = interp_eff(x) + eff_error_down = interp_eff_error_down(x) + eff_error_up = interp_eff_error_up(x) + eff_err = [eff_error_down, eff_error_up] + + # else pseudo integrate + else: + if centers: + M = len(f) + x = np.zeros((M, N)) + bin_centers = f-mix_freq + else: + M = len(f)-1 + x = x = np.zeros((M, N)) + bin_centers = f[0:-1]+0.5*df-mix_freq + + for i in range(M): + x[i] = np.linspace(bin_centers[i]-df/2, bin_centers[i]+df/2, N) + + + eff = interp_eff(x) + eff_err = [interp_eff_error_down(x), interp_eff_error_up(x)] + + + eff = np.sum(eff, axis=1)/N + #eff_err = [np.sqrt(np.sum(eff_err[0]**2, axis=1))/np.sqrt(N), + # np.sqrt(np.sum(eff_err[1]**2, axis=1))/np.sqrt(N)] + eff_err = [np.mean(eff_err[0], axis=1), np.mean(eff_err[1], axis=1)] + + + return eff, eff_err + + + +def pseudo_integrated_efficiency(f, df, snr_efficiency_dict, alpha=1): + if isinstance(f, float): + y, y_error = integrated_efficiency(f, snr_efficiency_dict, df) + pseudo_y = np.random.randn()*y_error# + y + else: + y, y_error = integrated_efficiency(f, snr_efficiency_dict, df) + + pseudo_y = np.random.randn(len(f)) + #print(pseudo_y) + pseudo_y[pseudo_y<0]*=y_error[0][pseudo_y<0] + pseudo_y[pseudo_y>0]*=y_error[1][pseudo_y>0] + #print(pseudo_y) + return pseudo_y+y, y_error + + + +def sinc(x): + if x == 0 : + return 1 + return np.sin(x)/x +def cot(x): + return np.cos(x)/np.sin(x) + +def alpha_function(n, L0, E, H, theta ): + gamma = 1 + E / me_keV + wc = q_C * H / ( gamma * m_kg ) + v0 = c * ( 1 - 1 / gamma ** 2 )**0.5 + wa = v0 / L0 * np.sin(theta) + #wa = v0 / L0 * np.mean(np.sin(theta)) + f = wc / ( 2 * np.pi ) + q = -0.25 * wc/wa * cot(theta)**2 + #q = -0.25 * wc/wa * np.mean(cot(theta)**2) + + return scs.jv( n , q ) + +def beta_function(n, L0, E, H, theta ): + + gamma = 1 + E / me_keV + wc = q_C * H / ( gamma * m_kg ) + v0 = c * ( 1 - 1 / gamma ** 2 )**0.5 + vz0 = v0 * np.cos(theta) + #vz0 = v0 * np.mean(np.cos(theta)) + wa = v0 / L0 * np.sin(theta) + #wa = v0 / L0 * np.mean(np.sin(theta)) + f = wc / ( 2 * np.pi ) + Dw = 0.5 * wc * cot(theta)**2 + #Dw = 0.5 * wc * np.mean(cot(theta)**2) + fc=c/(2*a) + vp = c/(1-(2*np.pi*fc/(wc+Dw))**2)**0.5 + k = (wc + Dw) / vp + zmax = L0 * cot( theta ) + #zmax = L0 * np.mean(cot( theta )) + + return scs.jv( n , k * zmax ) + +def freq_func( n, L0, E, H, theta ): + gamma = 1 + E / me_keV + wc = q_C * H / ( gamma * m_kg ) + v0 = c * ( 1 - 1 / gamma ** 2 )**0.5 + wa = v0 / L0 * np.sin(theta) + return (wc+n*wa)/(2*np.pi) + +#def power_function(n, L0, E, H, theta ): +# +# an = 0 +# for m in range( -5, 5 ): +# an += alpha_function ( n = m, L0 = L0 , E = E , H = H, theta = theta ) * beta_function( n = n - 2 * m, L0 = L0 , E = E , H = H, theta = theta ) +# return abs( an ) ** 2 + +def power_function(n, L0, E, H, theta): + zmax = L0 / np.tan(theta/180*np.pi) + gamma = 1 + E / me_keV + wc = q_C * H / ( gamma * m_kg ) + Dw = 0.5 * wc * cot(theta)**2 + fc=c/(2*a) + vp = c/(1-(2*np.pi*fc/(wc+Dw))**2)**0.5 + k = wc / c + return scs.jv(n, k*zmax)**2 + +def start_freq_func( L0, E, H, theta ): + gamma = 1 + E / me_keV + wc = q_C * H / ( gamma * m_kg ) + Dw = 0.5 * wc * cot(theta)**2 + #Dw = 0.5 * wc * np.mean(cot(theta)**2) + #print(Dw) + return ( wc + Dw ) / ( 2 * np.pi ) + +#def start_freq_func( L0, E, H, theta ): +# gamma = 1 + E / me_keV +# wc = q_C * H / ( gamma * m_kg ) +# Dw = 0.5 * wc * cot(theta)**2 +# #print Dw +# return ( wc + Dw ) / ( 2 * np.pi ) + +fc11 = 1.841 * c / (2 * np.pi * r) + + +def z_11(f): + return Z0 / (1 - (fc11/f)**2)**0.5 + +def p_11(f, E, rho): + gamma = 1 + E / me_keV + v0 = c * ( 1 - 1 / gamma ** 2 )**0.5 + + alpha = 0.108858 * r**2 + return z_11(f) * q_C**2 * v0**2 / (8 * np.pi * alpha) * ( scs.jvp(1, 1.841 * rho / r )**2 + 1 / (1.841 * rho / r )**2 * scs.j1(1.841 * rho / r)**2 ) + + +def Power_17keV(f): + f = np.array(f) + P = np.zeros(len(f)) + + # powers from ali + E = 17.826e3 + r = 0.0001 + n = 0 + + for i in range(len(f)): + h = B_field(E*1e-3, f[i]) + bla = start_freq_func(L0, E*1e-3, h, np.pi/2) + p = (p_11(bla, E*1e-3, r)) + + P[i] = p + + return P + + +def Power(f): + # powers from ali + f = np.array(f) + P = np.zeros(len(f)) + + E = energy(f) + r = 0.0001 + n = 0 + + + for i in range(len(E)): + p_dist = power_function(n, L0, E[i]*1e-3, H, np.pi/2) + bla = (start_freq_func(L0, E[i]*1e-3, H, np.pi/2)) + p = p_11(bla, E[i]*1e-3, r) + + P[i] = p + + return P + + +def energy(f, mixfreq=0): + + B = 0.9578170819250281 + emass = constants.electron_mass/constants.e*constants.c**2 + gamma = (constants.e*B)/(2.0*np.pi*constants.electron_mass) * 1/(f+mixfreq) + #shallow trap 0.959012745568 + #deep trap 0.95777194923080811 + return (gamma -1)*emass + + + +def B_field(e_keV, f): + emass = constants.electron_mass/constants.e*constants.c**2*1e-3 + b = (e_keV*1.0/emass + 1) * 2*np.pi*constants.electron_mass * f / constants.e + return b diff --git a/mermithid/misc/FakeTritiumDataFunctions.py b/mermithid/misc/FakeTritiumDataFunctions.py index 5e36fe6e..9665ff70 100644 --- a/mermithid/misc/FakeTritiumDataFunctions.py +++ b/mermithid/misc/FakeTritiumDataFunctions.py @@ -20,6 +20,8 @@ from mermithid.misc.Constants import * from mermithid.misc.ConversionFunctions import * +import matplotlib.pyplot as plt + """ Constants and functions used by processors/TritiumSpectrum/FakeDataGenerator.py """ @@ -272,15 +274,30 @@ def convolved_bkgd_rate(K, Kmin, Kmax, lineshape, ls_params, min_energy, max_ene return 0. + #Convolution of signal and lineshape using scipy.signal.convolve def convolved_spectral_rate_arrays(K, Q, mnu, Kmin, - lineshape, ls_params, min_energy, max_energy, - complexLineShape, final_state_array): + lineshape, ls_params, scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction, min_energy, max_energy, + complexLineShape, final_state_array, resolution_function, ins_res_width_bounds, ins_res_width_factors, p_factors, q_factors): """K is an array-like object """ logger.info('Using scipy convolve') + logger.info('Lineshape is {} with {}'.format(lineshape, resolution_function)) energy_half_range = max(max_energy, abs(min_energy)) + #logger.info('Using {} frequency regions. Mean and std of p are {} and {}. For q its {} and {}'.format(len(ins_res_width_bounds)-1, + # np.mean(p_factors), np.std(p_factors), + # np.mean(q_factors), np.std(q_factors))) + + if ins_res_width_bounds != None: + Kbounds = [np.min(K)] + ins_res_width_bounds + [np.max(K)] + else: + Kbounds = [np.min(K), np.max(K)] + + K_segments = [] + for i in range(len(Kbounds)-1): + K_segments.append(K[np.logical_and(Kbounds[i]<=K, K<=Kbounds[i+1])]) + dE = K[1] - K[0] n_dE_pos = round(energy_half_range/dE) #Number of steps for the lineshape for energies > 0 n_dE_neg = round(energy_half_range/dE) #Same, for energies < 0 @@ -293,23 +310,52 @@ def convolved_spectral_rate_arrays(K, Q, mnu, Kmin, elif lineshape=='simplified_scattering' or lineshape=='simplified': lineshape_rates = simplified_ls(K_lineshape, 0, ls_params[0], ls_params[1], ls_params[2], ls_params[3], ls_params[4], ls_params[5]) elif lineshape=='detailed_scattering' or lineshape=='detailed': + if resolution_function == 'simulated_resolution' or resolution_function == 'simulated': + lineshape_rates = [] + scale_factors = [ls_params[0]*f for f in ins_res_width_factors] + for i in range(len(scale_factors)): + lineshape_rates.append(np.flipud(complexLineShape.make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio(scale_factors[i], ls_params[1], scatter_peak_ratio_p*p_factors[i], scatter_peak_ratio_q*q_factors[i], scatter_fraction, emitted_peak='dirac'))) + elif resolution_function == 'gaussian_resolution' or resolution_function == 'gaussian': + logger.warn("Scatter peak ratio function for lineshape with Gaussian resolution may not be up-to-date!") + gaussian_widths = [ls_params[0]*f for f in ins_res_width_factors] + lineshape_rates = [np.flipud(complexLineShape.make_spectrum_gaussian_resolution_fit_scatter_peak_ratio(gaussian_widths[i], ls_params[1], scatter_peak_ratio_p*p_factors[i], scatter_peak_ratio_q*q_factors[i], scatter_fraction, emitted_peak='dirac')) for i in range(len(gaussian_widths))] + else: + logger.warn('{} is not a resolution function that has been implemented in the FakeDataGenerator'.format(resolution_function)) - lineshape_rates = complexLineShape.spectrum_func_1(K_lineshape/1000., ls_params[0], 0, 1, ls_params[1]) - - beta_rates = spectral_rate(K, Q, mnu, final_state_array) #np.zeros(len(K)) - #for i,ke in enumerate(K): - # beta_rates[i] = spectral_rate(ke, Q, mnu, final_state_array) + below_Kmin = np.where(K < Kmin) #Convolving - convolved = convolve(beta_rates, lineshape_rates, mode='same') - below_Kmin = np.where(K < Kmin) - np.put(convolved, below_Kmin, np.zeros(len(below_Kmin))) + if (lineshape=='detailed_scattering' or lineshape=='detailed'):# and (resolution_function == 'simulated_resolution' or resolution_function == 'simulated'): + convolved_segments = [] + beta_rates = spectral_rate(K, Q, mnu, final_state_array) + plt.figure(figsize=(7,5)) + for j in range(len(lineshape_rates)): + #beta_rates = spectral_rate(K_segments[j], Q, mnu, final_state_array) + plt.plot(lineshape_rates[j]) + convolved_j = convolve(beta_rates, lineshape_rates[j], mode='same') + np.put(convolved_j, below_Kmin, np.zeros(len(below_Kmin))) + #Only including the part of convolved_j that corresponds to the right values of K + convolved_segments.append(convolved_j[np.logical_and(Kbounds[j]<=K, K<=Kbounds[j+1])]) + #convolved.append(convolved_j) + convolved = np.concatenate(convolved_segments, axis=None) + plt.savefig('varied_lineshapes.png', dpi=200) + """elif resolution_function=='gaussian': + lineshape_rates = np.flipud(lineshape_rates) + beta_rates = spectral_rate(K, Q, mnu, final_state_array) + convolved = convolve(beta_rates, lineshape_rates, mode='same') + np.put(convolved, below_Kmin, np.zeros(len(below_Kmin)))""" + + if (lineshape=='gaussian' or lineshape=='simplified_scattering' or lineshape=='simplified'): + beta_rates = spectral_rate(K, Q, mnu, final_state_array) + convolved = convolve(beta_rates, lineshape_rates, mode='same') + np.put(convolved, below_Kmin, np.zeros(len(below_Kmin))) + return convolved #Convolution of background and lineshape using scipy.signal.convolve -def convolved_bkgd_rate_arrays(K, Kmin, Kmax, lineshape, ls_params, min_energy, max_energy, complexLineShape): +def convolved_bkgd_rate_arrays(K, Kmin, Kmax, lineshape, ls_params, scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction, min_energy, max_energy, complexLineShape, resolution_function): """K is an array-like object """ energy_half_range = max(max_energy, abs(min_energy)) @@ -325,7 +371,13 @@ def convolved_bkgd_rate_arrays(K, Kmin, Kmax, lineshape, ls_params, min_energy, elif lineshape=='simplified_scattering' or lineshape=='simplified': lineshape_rates = simplified_ls(K_lineshape, 0, ls_params[0], ls_params[1], ls_params[2], ls_params[3], ls_params[4], ls_params[5]) elif lineshape=='detailed_scattering' or lineshape=='detailed': - lineshape_rates = complexLineShape.spectrum_func_1(K_lineshape/1000., ls_params[0], 0, 1, ls_params[1]) + if resolution_function == 'simulated_resolution' or resolution_function == 'simulated': + lineshape_rates = complexLineShape.make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio(ls_params[0], ls_params[1], scatter_peak_ratio_p, scatter_peak_ratio_p, scatter_fraction, emitted_peak='dirac') + elif resolution_function == 'gaussian_resolution' or resolution_function == 'gaussian': + lineshape_rates = complexLineShape.make_spectrum_gaussian_resolution_fit_scatter_peak_ratio(ls_params[0], ls_params[1], scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction, emitted_peak='dirac') + else: + logger.warn('{} is not a resolution function that has been implemented in the FakeDataGenerator'.format(resolution_function)) + lineshape_rates = np.flipud(lineshape_rates) bkgd_rates = np.full(len(K), bkgd_rate()) if len(K) < len(K_lineshape): @@ -335,22 +387,32 @@ def convolved_bkgd_rate_arrays(K, Kmin, Kmax, lineshape, ls_params, min_energy, convolved = convolve(bkgd_rates, lineshape_rates, mode='same') below_Kmin = np.where(K < Kmin) np.put(convolved, below_Kmin, np.zeros(len(below_Kmin))) + return convolved -##Fraction of events near the endpoint -##Currently, this only holds for the last 13.6 eV of the spectrum -#def frac_near_endpt(Kmin, Q, mass, atom_or_mol='atom'): -# A = integrate.quad(spectral_rate, Kmin, Q-mass, args=(Q,mass)) -# B = integrate.quad(spectral_rate, V0, Q-mass, args=(Q,mass)) #Minimum at V0 because electrons with energy below screening barrier do not escape -# f = (A[0])/(B[0]) -# if atom_or_mol=='atom': -# return 0.7006*f -# elif atom_or_mol=='mol' or atom_or_mol=='molecule': -# return 0.57412*f -# else: -# print("Choose 'atom' or 'mol'.") +#Fraction of events near the endpoint +def frac_near_endpt(Kmin, Q, mass, final_state_array, atom_or_mol='mol', range='wide'): + """ + Options for range: + - 'narrow': Only extends ~18 eV (or less) below the endpoint, so that all decays are to the ground state + - 'wide': Wide enough that the probability of decay to a 3He electronic energy level that would shift Q below the ROI is very low + """ + A = integrate.quad(spectral_rate, Kmin, Q-mass, args=(Q, mass, final_state_array)) + B = integrate.quad(spectral_rate, V0, Q-mass, args=(Q, mass, final_state_array)) #Minimum at V0 because electrons with energy below screening barrier do not escape + f = (A[0])/(B[0]) + if range=='narrow': + if atom_or_mol=='atom': + return 0.7006*f + elif atom_or_mol=='mol' or atom_or_mol=='molecule': + return 0.57412*f + else: + logger.warn("Choose 'atom' or 'mol'.") + elif range=='wide': + return f + else: + logger.warn("Choose range 'narrow' or 'wide'") #Convert [number of particles]=(density*volume*efficiency) to a signal activity A_s, measured in events/second. @@ -358,7 +420,7 @@ def find_signal_activity(Nparticles, m, Q, Kmin, atom_or_mol='atom', nTperMolecu """ Functions to calculate number of events to generate """ - br = frac_near_endpt(Kmin, Q, m, atom_or_mol) + br = frac_near_endpt(Kmin, Q, m, final_state_array, atom_or_mol) Thalflife = 3.8789*10**8 A_s = Nparticles*np.log(2)/(Thalflife)*br if atom_or_mol=='atom': @@ -383,3 +445,17 @@ def efficiency_from_interpolation(x, efficiency_dict, B=0.9578186017836624): +def random_efficiency_from_interpolation(x, efficiency_dict, B=0.9578186017836624): + """ + Function to calculate efficiency + """ + logger.info('Sampling efficiencies before interpolation') + f = Frequency(x, B) + + efficiency_mean = efficiency_dict['eff interp with slope correction'] + efficiency_error = np.mean(efficiency_dict['error interp with slope correction'], axis=0) + random_efficiencies = np.random.normal(efficiency_mean, efficiency_error) + random_efficiencies[random_efficiencies<0] = 0. + interp_efficiency = interp1d(efficiency_dict['frequencies'], random_efficiencies, fill_value='0', bounds_error=False) + + return interp_efficiency(f) \ No newline at end of file diff --git a/mermithid/misc/__init__.py b/mermithid/misc/__init__.py index afb4ebbe..8654ab4e 100644 --- a/mermithid/misc/__init__.py +++ b/mermithid/misc/__init__.py @@ -8,3 +8,4 @@ from . import TritiumFormFactor from . import FakeTritiumDataFunctions from . import ConversionFunctions +from . import DetectionEfficiencyUtilities diff --git a/mermithid/misc/saenz_mfs.json b/mermithid/misc/saenz_mfs.json index 3d3b9d7d..89524096 100644 --- a/mermithid/misc/saenz_mfs.json +++ b/mermithid/misc/saenz_mfs.json @@ -1 +1 @@ -{"Binding energy": [1.897, 1.844, 1.773, 1.65, 1.546, 1.455, 1.341, 1.232, 1.138, 1.047, 0.96, 0.849, 0.754, 0.647999999999999, 0.538, 0.446, 0.345, 0.24, 0.151999999999999, 0.0629999999999999, -0.0429999999999999, -0.147, -0.247, -0.347, -0.446999999999999, -0.613, -0.865, -1.112, -1.36, -1.61, -1.86, -2.186, -2.68199999999999, -3.23499999999999, -3.75, -16.603, -17.603, -18.799, -19.761, -20.73, -21.701, -22.676, -23.653, -24.632, -25.613, -26.596, -27.581, -28.567, -29.558, -30.593, -31.66, -32.637, -33.595, -34.562, -35.548, -36.566, -37.602, -38.609, -39.601, -40.601, -41.607, -42.614, -43.597, -44.584, -45.586, -46.616, -47.601, -48.565, -49.604, -50.599, -51.594, -52.605, -53.611, -54.629, -55.621, -56.632, -57.621, -58.608, -59.608, -60.604, -61.133, -62.615, -63.607, -64.613, -65.604, -66.595, -67.592, -68.589, -69.5759999999999, -70.5809999999999, -71.589, -72.5459999999999, -73.5489999999999, -74.568, -75.533, -76.6149999999999, -77.5669999999999, -78.607, -79.613, -80.6259999999999, -81.6079999999999, -82.6019999999999, -83.5929999999999, -84.5939999999999, -85.601, -86.601, -87.598, -89.0699999999999, -91.086, -93.0849999999999, -95.0849999999999, -97.084, -99.084, -101.086, -103.087, -105.088, -107.089, -109.089, -111.09, -113.09, -115.09, -117.091, -119.091, -121.091, -123.092, -125.092, -127.092, -129.092, -131.093, -133.093, -135.093, -137.093, -140.065, -144.067, -148.068, -152.069, -156.07, -160.072, -164.073, -168.074, -172.075, -176.076, -180.076, -184.077, -188.078, -192.079, -196.079, -200.08, -204.08, -208.081, -212.082, -216.082, -220.082, -224.083, -228.083, -232.084, -236.084], "Probability": [9.99999999999999e-08, 0.0006900000000000001, 0.00046, 0.00233, 0.00553, 0.00457, 0.02033, 0.01649, 0.03877, 0.038079999999999996, 0.06809, 0.11214, 0.10112, 0.24406, 0.32337000000000005, 0.40864, 0.68745, 0.66279, 0.51412, 0.6556100000000001, 0.54588, 0.37231000000000003, 0.25473, 0.16959, 0.11369, 0.16946999999999998, 0.10094, 0.05732, 0.02806, 0.013160000000000002, 0.00623, 0.0042, 0.0008, 0.00015, 0.0, 0.0, 0.0, 1.1999999999999999e-05, 0.000113, 0.0006560000000000001, 0.002567, 0.007149, 0.014804, 0.023583, 0.029715, 0.030307, 0.025527, 0.018080000000000002, 0.01107, 0.007377000000000001, 0.010637, 0.019095, 0.022178, 0.016434, 0.009037, 0.004989, 0.003978, 0.004124, 0.004152, 0.0039250000000000005, 0.003457, 0.003186, 0.0027010000000000003, 0.0027129999999999997, 0.002481, 0.002412, 0.001907, 0.001938, 0.0017599999999999998, 0.001575, 0.0015409999999999998, 0.001485, 0.001557, 0.001895, 0.002427, 0.003357, 0.004095, 0.004714, 0.005033999999999999, 0.005152, 0.005442000000000001, 0.005859, 0.006617, 0.0070940000000000005, 0.007404, 0.007164, 0.006563, 0.005620000000000001, 0.004691, 0.00368, 0.003049, 0.00221, 0.001928, 0.0017610000000000002, 0.0015300000000000001, 0.001215, 0.0013900000000000002, 0.001216, 0.0014219999999999999, 0.001384, 0.001368, 0.001316, 0.001153, 0.0010760000000000001, 0.000921, 0.0007570000000000001, 0.000696, 0.0006180000000000001, 0.00054, 0.00048300000000000003, 0.00043200000000000004, 0.000388, 0.00035150000000000003, 0.00031800000000000003, 0.000289, 0.000264, 0.00024150000000000002, 0.000222, 0.0002045, 0.000189, 0.00017500000000000003, 0.00016250000000000002, 0.000151, 0.000141, 0.0001315, 0.000123, 0.000115, 0.00010800000000000001, 0.0001015, 9.549999999999999e-05, 8.999999999999999e-05, 8.45e-05, 7.775e-05, 6.95e-05, 6.25e-05, 5.625e-05, 5.1000000000000006e-05, 4.625e-05, 4.225e-05, 3.85e-05, 3.525e-05, 3.25e-05, 3e-05, 2.775e-05, 2.5750000000000002e-05, 2.375e-05, 2.225e-05, 2.075e-05, 1.925e-05, 1.8e-05, 1.7e-05, 1.6000000000000003e-05, 1.5e-05, 1.4e-05, 1.325e-05, 1.1750000000000001e-05, NaN]} \ No newline at end of file +{"Binding energy": [1.897, 1.844, 1.773, 1.65, 1.546, 1.455, 1.341, 1.232, 1.138, 1.047, 0.96, 0.849, 0.754, 0.647999999999999, 0.538, 0.446, 0.345, 0.24, 0.151999999999999, 0.0629999999999999, -0.0429999999999999, -0.147, -0.247, -0.347, -0.446999999999999, -0.613, -0.865, -1.112, -1.36, -1.61, -1.86, -2.186, -2.68199999999999, -3.23499999999999, -3.75, -16.603, -17.603, -18.799, -19.761, -20.73, -21.701, -22.676, -23.653, -24.632, -25.613, -26.596, -27.581, -28.567, -29.558, -30.593, -31.66, -32.637, -33.595, -34.562, -35.548, -36.566, -37.602, -38.609, -39.601, -40.601, -41.607, -42.614, -43.597, -44.584, -45.586, -46.616, -47.601, -48.565, -49.604, -50.599, -51.594, -52.605, -53.611, -54.629, -55.621, -56.632, -57.621, -58.608, -59.608, -60.604, -61.133, -62.615, -63.607, -64.613, -65.604, -66.595, -67.592, -68.589, -69.5759999999999, -70.5809999999999, -71.589, -72.5459999999999, -73.5489999999999, -74.568, -75.533, -76.6149999999999, -77.5669999999999, -78.607, -79.613, -80.6259999999999, -81.6079999999999, -82.6019999999999, -83.5929999999999, -84.5939999999999, -85.601, -86.601, -87.598, -89.0699999999999, -91.086, -93.0849999999999, -95.0849999999999, -97.084, -99.084, -101.086, -103.087, -105.088, -107.089, -109.089, -111.09, -113.09, -115.09, -117.091, -119.091, -121.091, -123.092, -125.092, -127.092, -129.092, -131.093, -133.093, -135.093, -137.093, -140.065, -144.067, -148.068, -152.069, -156.07, -160.072, -164.073, -168.074, -172.075, -176.076, -180.076, -184.077, -188.078, -192.079, -196.079, -200.08, -204.08, -208.081, -212.082, -216.082, -220.082, -224.083, -228.083, -232.084, -236.084], "Probability": [9.99999999999999e-08, 0.0006900000000000001, 0.00046, 0.00233, 0.00553, 0.00457, 0.02033, 0.01649, 0.03877, 0.038079999999999996, 0.06809, 0.11214, 0.10112, 0.24406, 0.32337000000000005, 0.40864, 0.68745, 0.66279, 0.51412, 0.6556100000000001, 0.54588, 0.37231000000000003, 0.25473, 0.16959, 0.11369, 0.16946999999999998, 0.10094, 0.05732, 0.02806, 0.013160000000000002, 0.00623, 0.0042, 0.0008, 0.00015, 0.0, 0.0, 0.0, 1.1999999999999999e-05, 0.000113, 0.0006560000000000001, 0.002567, 0.007149, 0.014804, 0.023583, 0.029715, 0.030307, 0.025527, 0.018080000000000002, 0.01107, 0.007377000000000001, 0.010637, 0.019095, 0.022178, 0.016434, 0.009037, 0.004989, 0.003978, 0.004124, 0.004152, 0.0039250000000000005, 0.003457, 0.003186, 0.0027010000000000003, 0.0027129999999999997, 0.002481, 0.002412, 0.001907, 0.001938, 0.0017599999999999998, 0.001575, 0.0015409999999999998, 0.001485, 0.001557, 0.001895, 0.002427, 0.003357, 0.004095, 0.004714, 0.005033999999999999, 0.005152, 0.005442000000000001, 0.005859, 0.006617, 0.0070940000000000005, 0.007404, 0.007164, 0.006563, 0.005620000000000001, 0.004691, 0.00368, 0.003049, 0.00221, 0.001928, 0.0017610000000000002, 0.0015300000000000001, 0.001215, 0.0013900000000000002, 0.001216, 0.0014219999999999999, 0.001384, 0.001368, 0.001316, 0.001153, 0.0010760000000000001, 0.000921, 0.0007570000000000001, 0.000696, 0.0006180000000000001, 0.00054, 0.00048300000000000003, 0.00043200000000000004, 0.000388, 0.00035150000000000003, 0.00031800000000000003, 0.000289, 0.000264, 0.00024150000000000002, 0.000222, 0.0002045, 0.000189, 0.00017500000000000003, 0.00016250000000000002, 0.000151, 0.000141, 0.0001315, 0.000123, 0.000115, 0.00010800000000000001, 0.0001015, 9.549999999999999e-05, 8.999999999999999e-05, 8.45e-05, 7.775e-05, 6.95e-05, 6.25e-05, 5.625e-05, 5.1000000000000006e-05, 4.625e-05, 4.225e-05, 3.85e-05, 3.525e-05, 3.25e-05, 3e-05, 2.775e-05, 2.5750000000000002e-05, 2.375e-05, 2.225e-05, 2.075e-05, 1.925e-05, 1.8e-05, 1.7e-05, 1.6000000000000003e-05, 1.5e-05, 1.4e-05, 1.325e-05, 1.25e-05, 1.1750000000000001e-05]} diff --git a/mermithid/misc/saenz_mfs_ctd.json b/mermithid/misc/saenz_mfs_ctd.json new file mode 100644 index 00000000..1c78c976 --- /dev/null +++ b/mermithid/misc/saenz_mfs_ctd.json @@ -0,0 +1 @@ +{"Binding energy": [1.897, 1.844, 1.773, 1.65, 1.546, 1.455, 1.341, 1.232, 1.138, 1.047, 0.96, 0.849, 0.754, 0.647999999999999, 0.538, 0.446, 0.345, 0.24, 0.151999999999999, 0.0629999999999999, -0.0429999999999999, -0.147, -0.247, -0.347, -0.446999999999999, -0.613, -0.865, -1.112, -1.36, -1.61, -1.86, -2.186, -2.68199999999999, -3.23499999999999, -3.75, -16.603, -17.603, -18.799, -19.761, -20.73, -21.701, -22.676, -23.653, -24.632, -25.613, -26.596, -27.581, -28.567, -29.558, -30.593, -31.66, -32.637, -33.595, -34.562, -35.548, -36.566, -37.602, -38.609, -39.601, -40.601, -41.607, -42.614, -43.597, -44.584, -45.586, -46.616, -47.601, -48.565, -49.604, -50.599, -51.594, -52.605, -53.611, -54.629, -55.621, -56.632, -57.621, -58.608, -59.608, -60.604, -61.133, -62.615, -63.607, -64.613, -65.604, -66.595, -67.592, -68.589, -69.5759999999999, -70.5809999999999, -71.589, -72.5459999999999, -73.5489999999999, -74.568, -75.533, -76.6149999999999, -77.5669999999999, -78.607, -79.613, -80.6259999999999, -81.6079999999999, -82.6019999999999, -83.5929999999999, -84.5939999999999, -85.601, -86.601, -87.598, -89.0699999999999, -91.086, -93.0849999999999, -95.0849999999999, -97.084, -99.084, -101.086, -103.087, -105.088, -107.089, -109.089, -111.09, -113.09, -115.09, -117.091, -119.091, -121.091, -123.092, -125.092, -127.092, -129.092, -131.093, -133.093, -135.093, -137.093, -140.065, -144.067, -148.068, -152.069, -156.07, -160.072, -164.073, -168.074, -172.075, -176.076, -180.076, -184.077, -188.078, -192.079, -196.079, -200.08, -204.08, -208.081, -212.082, -216.082, -220.082, -224.083, -228.083, -232.084, -236.084, -238.103, -248.103, -258.103, -268.103, -278.103, -288.103, -298.103, -308.103, -318.103, -328.103, -338.103, -348.103, -358.103, -368.103, -378.103, -388.103, -398.103, -408.103, -418.103, -428.103, -438.103, -448.103, -458.103, -468.103, -478.103, -488.103, -498.103, -508.103, -518.103, -528.103, -538.103, -548.103, -558.103, -568.103, -578.103, -588.103, -598.103, -608.103, -618.103, -628.103, -638.103, -648.103, -658.103, -668.103, -678.103, -688.103, -698.103, -708.103, -718.103, -728.103, -738.103, -748.103, -758.103, -768.103, -778.103, -788.103, -798.103, -808.103, -818.103, -828.103, -838.103, -848.103, -858.103, -868.103, -878.103, -888.103, -898.103, -908.103, -918.103, -928.103, -938.103, -948.103, -958.103, -968.103, -978.103, -988.103, -998.103, -1008.103, -1018.103, -1028.103, -1038.103, -1048.103, -1058.103, -1068.103, -1078.103, -1088.103, -1098.103, -1108.103, -1118.103, -1128.103, -1138.103, -1148.103, -1158.103, -1168.103, -1178.103, -1188.103, -1198.103, -1208.103, -1218.103, -1228.103, -1238.103, -1248.103, -1258.103, -1268.103, -1278.103, -1288.103, -1298.103, -1308.103, -1318.103, -1328.103, -1338.103, -1348.103, -1358.103, -1368.103, -1378.103, -1388.103, -1398.103, -1408.103, -1418.103, -1428.103, -1438.103, -1448.103, -1458.103, -1468.103, -1478.103, -1488.103, -1498.103, -1508.103, -1518.103, -1528.103, -1538.103, -1548.103, -1558.103, -1568.103, -1578.103, -1588.103, -1598.103, -1608.103, -1618.103, -1628.103, -1638.103, -1648.103, -1658.103, -1668.103, -1678.103, -1688.103, -1698.103, -1708.103, -1718.103, -1728.103, -1738.103, -1748.103, -1758.103, -1768.103, -1778.103, -1788.103, -1798.103, -1808.103, -1818.103, -1828.103, -1838.103, -1848.103, -1858.103, -1868.103, -1878.103, -1888.103, -1898.103, -1908.103, -1918.103, -1928.103, -1938.103, -1948.103, -1958.103, -1968.103, -1978.103, -1988.103, -1998.103, -2008.103, -2018.103, -2028.103, -2038.103, -2048.103, -2058.103, -2068.103, -2078.103, -2088.103, -2098.103, -2108.103, -2118.103, -2128.103, -2138.103, -2148.103, -2158.103, -2168.103, -2178.103, -2188.103, -2198.103, -2208.103, -2218.103, -2228.103, -2238.103, -2248.103, -2258.103, -2268.103, -2278.103, -2288.103], "Probability": [9.99999999999999e-08, 0.0006900000000000001, 0.00046, 0.00233, 0.00553, 0.00457, 0.02033, 0.01649, 0.03877, 0.038079999999999996, 0.06809, 0.11214, 0.10112, 0.24406, 0.32337000000000005, 0.40864, 0.68745, 0.66279, 0.51412, 0.6556100000000001, 0.54588, 0.37231000000000003, 0.25473, 0.16959, 0.11369, 0.16946999999999998, 0.10094, 0.05732, 0.02806, 0.013160000000000002, 0.00623, 0.0042, 0.0008, 0.00015, 0.0, 0.0, 0.0, 1.1999999999999999e-05, 0.000113, 0.0006560000000000001, 0.002567, 0.007149, 0.014804, 0.023583, 0.029715, 0.030307, 0.025527, 0.018080000000000002, 0.01107, 0.007377000000000001, 0.010637, 0.019095, 0.022178, 0.016434, 0.009037, 0.004989, 0.003978, 0.004124, 0.004152, 0.0039250000000000005, 0.003457, 0.003186, 0.0027010000000000003, 0.0027129999999999997, 0.002481, 0.002412, 0.001907, 0.001938, 0.0017599999999999998, 0.001575, 0.0015409999999999998, 0.001485, 0.001557, 0.001895, 0.002427, 0.003357, 0.004095, 0.004714, 0.005033999999999999, 0.005152, 0.005442000000000001, 0.005859, 0.006617, 0.0070940000000000005, 0.007404, 0.007164, 0.006563, 0.005620000000000001, 0.004691, 0.00368, 0.003049, 0.00221, 0.001928, 0.0017610000000000002, 0.0015300000000000001, 0.001215, 0.0013900000000000002, 0.001216, 0.0014219999999999999, 0.001384, 0.001368, 0.001316, 0.001153, 0.0010760000000000001, 0.000921, 0.0007570000000000001, 0.000696, 0.0006180000000000001, 0.00054, 0.00048300000000000003, 0.00043200000000000004, 0.000388, 0.00035150000000000003, 0.00031800000000000003, 0.000289, 0.000264, 0.00024150000000000002, 0.000222, 0.0002045, 0.000189, 0.00017500000000000003, 0.00016250000000000002, 0.000151, 0.000141, 0.0001315, 0.000123, 0.000115, 0.00010800000000000001, 0.0001015, 9.549999999999999e-05, 8.999999999999999e-05, 8.45e-05, 7.775e-05, 6.95e-05, 6.25e-05, 5.625e-05, 5.1000000000000006e-05, 4.625e-05, 4.225e-05, 3.85e-05, 3.525e-05, 3.25e-05, 3e-05, 2.775e-05, 2.5750000000000002e-05, 2.375e-05, 2.225e-05, 2.075e-05, 1.925e-05, 1.8e-05, 1.7e-05, 1.6000000000000003e-05, 1.5e-05, 1.4e-05, 1.325e-05, 1.25e-05, 1.1750000000000001e-05, 1.10288508e-05, 9.70207309e-06, 8.57766708e-06, 7.61863494e-06, 6.79579478e-06, 6.08592095e-06, 5.47037339e-06, 4.93407595e-06, 4.46474684e-06, 4.05231317e-06, 3.68846160e-06, 3.36629039e-06, 3.08003795e-06, 2.82486935e-06, 2.59670729e-06, 2.39209743e-06, 2.20810027e-06, 2.04220408e-06, 1.89225409e-06, 1.75639484e-06, 1.63302290e-06, 1.52074791e-06, 1.41836036e-06, 1.32480479e-06, 1.23915746e-06, 1.16060760e-06, 1.08844166e-06, 1.02203004e-06, 9.60815818e-07, 9.04305156e-07, 8.52059172e-07, 8.03686951e-07, 7.58839553e-07, 7.17204875e-07, 6.78503203e-07, 6.42483371e-07, 6.08919436e-07, 5.77607778e-07, 5.48364581e-07, 5.21023630e-07, 4.95434382e-07, 4.71460278e-07, 4.48977254e-07, 4.27872435e-07, 4.08042979e-07, 3.89395057e-07, 3.71842946e-07, 3.55308232e-07, 3.39719090e-07, 3.25009655e-07, 3.11119455e-07, 2.97992900e-07, 2.85578837e-07, 2.73830140e-07, 2.62703349e-07, 2.52158340e-07, 2.42158035e-07, 2.32668134e-07, 2.23656879e-07, 2.15094834e-07, 2.06954696e-07, 1.99211112e-07, 1.91840523e-07, 1.84821017e-07, 1.78132195e-07, 1.71755055e-07, 1.65671881e-07, 1.59866140e-07, 1.54322396e-07, 1.49026222e-07, 1.43964128e-07, 1.39123490e-07, 1.34492483e-07, 1.30060028e-07, 1.25815736e-07, 1.21749857e-07, 1.17853236e-07, 1.14117272e-07, 1.10533879e-07, 1.07095450e-07, 1.03794826e-07, 1.00625262e-07, 9.75804056e-08, 9.46542644e-08, 9.18411867e-08, 8.91358374e-08, 8.65331784e-08, 8.40284491e-08, 8.16171491e-08, 7.92950220e-08, 7.70580397e-08, 7.49023889e-08, 7.28244573e-08, 7.08208221e-08, 6.88882381e-08, 6.70236272e-08, 6.52240686e-08, 6.34867894e-08, 6.18091563e-08, 6.01886672e-08, 5.86229437e-08, 5.71097242e-08, 5.56468574e-08, 5.42322958e-08, 5.28640901e-08, 5.15403840e-08, 5.02594086e-08, 4.90194782e-08, 4.78189855e-08, 4.66563972e-08, 4.55302507e-08, 4.44391498e-08, 4.33817614e-08, 4.23568124e-08, 4.13630861e-08, 4.03994200e-08, 3.94647025e-08, 3.85578705e-08, 3.76779070e-08, 3.68238389e-08, 3.59947345e-08, 3.51897016e-08, 3.44078860e-08, 3.36484689e-08, 3.29106658e-08, 3.21937246e-08, 3.14969239e-08, 3.08195721e-08, 3.01610052e-08, 2.95205864e-08, 2.88977041e-08, 2.82917711e-08, 2.77022234e-08, 2.71285193e-08, 2.65701380e-08, 2.60265790e-08, 2.54973612e-08, 2.49820219e-08, 2.44801160e-08, 2.39912151e-08, 2.35149073e-08, 2.30507957e-08, 2.25984985e-08, 2.21576477e-08, 2.17278892e-08, 2.13088815e-08, 2.09002958e-08, 2.05018149e-08, 2.01131331e-08, 1.97339558e-08, 1.93639987e-08, 1.90029874e-08, 1.86506574e-08, 1.83067534e-08, 1.79710289e-08, 1.76432458e-08, 1.73231744e-08, 1.70105927e-08, 1.67052864e-08, 1.64070482e-08, 1.61156781e-08, 1.58309825e-08, 1.55527744e-08, 1.52808729e-08, 1.50151031e-08, 1.47552959e-08, 1.45012876e-08, 1.42529198e-08, 1.40100394e-08, 1.37724979e-08, 1.35401518e-08, 1.33128621e-08, 1.30904941e-08, 1.28729175e-08, 1.26600059e-08, 1.24516370e-08, 1.22476921e-08, 1.20480564e-08, 1.18526183e-08, 1.16612701e-08, 1.14739068e-08, 1.12904270e-08, 1.11107322e-08, 1.09347268e-08, 1.07623182e-08, 1.05934164e-08, 1.04279341e-08, 1.02657867e-08, 1.01068918e-08, 9.95116964e-09, 9.79854271e-09, 9.64893572e-09, 9.50227555e-09, 9.35849116e-09, 9.21751355e-09, 9.07927567e-09, 8.94371237e-09, 8.81076032e-09, 8.68035797e-09, 8.55244548e-09, 8.42696466e-09, 8.30385896e-09, 8.18307335e-09, 8.06455432e-09, 7.94824981e-09, 7.83410920e-09]} diff --git a/mermithid/processors/Fitters/BinnedDataFitter.py b/mermithid/processors/Fitters/BinnedDataFitter.py index edd2faac..285dd607 100644 --- a/mermithid/processors/Fitters/BinnedDataFitter.py +++ b/mermithid/processors/Fitters/BinnedDataFitter.py @@ -11,12 +11,16 @@ from __future__ import absolute_import import numpy as np -import scipy -import sys -import json +#import scipy +from scipy.optimize import NonlinearConstraint +#import sys +#import json from iminuit import Minuit +from iminuit.cost import LeastSquares from morpho.utilities import morphologging, reader from morpho.processors import BaseProcessor +from scipy.stats import chisquare +from scipy.special import factorial logger = morphologging.getLogger(__name__) @@ -57,12 +61,20 @@ def InternalConfigure(self, params): self.initial_values = reader.read_param(params, 'initial_values', [1]*len(self.parameter_names)) self.limits = reader.read_param(params, 'limits', [[None, None]]*len(self.parameter_names)) self.fixes = reader.read_param(params, 'fixed', [False]*len(self.parameter_names)) + self.fixes_dict = reader.read_param(params, 'fixed_parameter_dict', {}) self.bins = reader.read_param(params, 'bins', np.linspace(-2, 2, 100)) self.binned_data = reader.read_param(params, 'binned_data', False) self.print_level = reader.read_param(params, 'print_level', 1) self.constrained_parameters = reader.read_param(params, 'constrained_parameter_indices', []) - self.constrained_means = reader.read_param(params, 'constrained_parameter_means', []) - self.constrained_widths = reader.read_param(params, 'constrained_parameter_widths', []) + self.constrained_means = np.array(reader.read_param(params, 'constrained_parameter_means', [])) + self.constrained_widths = np.array(reader.read_param(params, 'constrained_parameter_widths', [])) + self.correlated_parameters = reader.read_param(params, 'correlated_parameter_indices', []) + self.cov_matrix = np.array(reader.read_param(params, 'covariance_matrix', [])) + self.minos_cls = reader.read_param(params, 'minos_confidence_level_list', []) + self.minos_intervals = reader.read_param(params,'find_minos_intervals', False) + self.free_in_second_fit = reader.read_param(params, 'free_in_second_fit', []) + self.fixed_in_second_fit = reader.read_param(params, 'fixed_in_second_fit', []) + self.error_def = reader.read_param(params,'error_def', 1) # derived configurations self.bin_centers = self.bins[0:-1]+0.5*(self.bins[1]-self.bins[0]) @@ -83,6 +95,8 @@ def InternalRun(self): logger.info('Data is unbinned. Will histogram before fitting.') self.hist, _ = np.histogram(self.data[self.namedata], self.bins) + logger.info('Total counts: {}'.format(np.sum(self.hist))) + result_array, error_array = self.fit() # save results @@ -91,7 +105,7 @@ def InternalRun(self): self.results['param_errors'] = error_array self.results['correlation_matrix'] = np.array(self.m_binned.covariance.correlation()) for i, k in enumerate(self.parameter_names): - self.results[k] = {'value': result_array[i], 'error': error_array[i]} + self.results[k] = {'value': result_array[i], 'error': error_array[i], 'likelihood': self.likelihood} return True @@ -105,51 +119,132 @@ def model(self, x, A, mu, sigma): return f + def setup_minuit(self): + + if self.error_def == 0.5: + self.m_binned = Minuit(self.negPoissonLogLikelihood, + self.initial_values, + name=self.parameter_names, + ) + + self.m_binned.errordef = 0.5 + logger.info('Doing MLL analysis') + else: + #least_squares = LeastSquares(self.bin_centers, self.hist, np.sqrt(self.hist), self.model) + self.m_binned = Minuit(self.leastSquares, + self.initial_values, + name=self.parameter_names) + self.m_binned.errordef = 1.0 + logger.info('Doing chi square analysis') + + self.m_binned.errors = self.parameter_errors + self.m_binned.throw_nan = False + self.m_binned.strategy = 1 + self.m_binned.print_level = self.print_level + + self.constraints = [] + + for i, name in enumerate(self.parameter_names): + if name in self.fixes_dict.keys(): + self.m_binned.fixed[name]= self.fixes_dict[name] + #logger.info('Fixing {}'.format(name)) + else: + self.m_binned.fixed[name] = self.fixes[i] + self.m_binned.limits[name] = self.limits[i] + #if not all(l is None for l in self.limits[i]): + # self.constraints.append(NonlinearConstraint(lambda x: x, self.limits[i][0], self.limits[i][1])) + def fit(self): # Now minimize neg log likelihood using iMinuit + self.setup_minuit() + + if self.print_level > 0: logger.info('This is the plan:') - logger.info('Fitting data consisting of {} elements'.format(np.sum(self.hist))) - logger.info('Fit parameters: {}'.format(self.parameter_names)) - logger.info('Initial values: {}'.format(self.initial_values)) - logger.info('Initial error: {}'.format(self.parameter_errors)) - logger.info('Limits: {}'.format(self.limits)) - logger.info('Fixed in fit: {}'.format(self.fixes)) - logger.info('Constrained parameters: {}'.format([self.parameter_names[i] for i in self.constrained_parameters])) - - m_binned = Minuit(self.negPoissonLogLikelihood, - self.initial_values, - # error=self.parameter_errors, - # errordef = 0.5, limit = self.limits, - name=self.parameter_names, - # fix=self.fixes, - # print_level=self.print_level, - # throw_nan=True - ) - - m_binned.errordef = 0.5 - m_binned.errors = self.parameter_errors - m_binned.throw_nan = True - m_binned.print_level = self.print_level + logger.info('\tFitting data consisting of {} elements'.format(np.sum(self.hist))) + logger.info('\tFit parameters: {}'.format(self.parameter_names)) + logger.info('\tInitial values: {}'.format(self.initial_values)) + logger.info('\tInitial error: {}'.format(self.parameter_errors)) + logger.info('\tLimits: {}'.format(self.limits)) + logger.info('\tFixed in fit: {}'.format(self.fixes)) + logger.info('\tConstrained parameters: {}'.format([self.parameter_names[i] for i in self.constrained_parameters])) + logger.info('\tConstraint means: {}'.format(self.constrained_means)) + logger.info('\tConstraint widths: {}'.format(self.constrained_widths)) + logger.info('\tCorrelated parameters: {}'.format([self.parameter_names[i] for i in self.correlated_parameters])) + logger.info('\tError def : {}'.format(self.m_binned.errordef)) + logger.info('\tMinos uncertainties: {}'.format(self.minos_cls)) - for i, name in enumerate(self.parameter_names): - m_binned.fixed[name] = self.fixes[i] - m_binned.limits[name] = self.limits[i] # minimze - m_binned.simplex().migrad() - m_binned.hesse() + self.m_binned.simplex().migrad() #scipy(constraints=self.constraints) + if len(self.free_in_second_fit) > 0: + for p in self.free_in_second_fit: + self.m_binned.fixed[p] = False + logger.info('{} now free.'.format(self.free_in_second_fit)) + if len(self.fixed_in_second_fit) > 0: + for p in self.fixed_in_second_fit: + self.m_binned.fixed[p] = True + #self.m_binned.limits[p] = [None, None] + logger.info('{} now fixed'.format(self.fixed_in_second_fit)) + if len(self.free_in_second_fit) > 0 or len(self.fixed_in_second_fit) > 0: + logger.info('Minimize again') + self.m_binned.migrad() + self.m_binned.hesse() + #m_binned.minos() #self.param_states = m_binned.get_param_states() - self.m_binned = m_binned + if self.print_level: + logger.info(self.m_binned.params) + logger.info(self.m_binned.values) + logger.info(self.m_binned.errors) + # results - result_array = np.array(m_binned.values) - error_array = np.array(m_binned.errors) + result_array = np.array(self.m_binned.values) + error_array = np.array(self.m_binned.errors) + self.likelihood = self.PoissonLogLikelihood(result_array) + + + if self.minos_intervals: + self.minos_errors = {} + for mcl in self.minos_cls: + logger.info('Getting minos errors for CL = {}'.format(mcl)) + try: + self.m_binned.minos(cl=mcl) + + self.minos_errors[mcl] = {} + if self.print_level: + logger.info(self.m_binned.params) + logger.info(self.m_binned.merrors) + for k in self.m_binned.merrors.keys(): + self.minos_errors[mcl][k] = {'interval': [self.m_binned.merrors[k].lower, self.m_binned.merrors[k].upper], + 'number': self.m_binned.merrors[k].number, + 'name': self.m_binned.merrors[k].name, + 'is_valid': self.m_binned.merrors[k].is_valid and self.m_binned.valid} + + except RuntimeError as e: + print(self.m_binned.params) + print(self.m_binned.merrors) + logger.error('Minos failed. Returning Hesse instead') + self.minos_errors[mcl] = {} + for i in range(len(self.parameter_names)): + self.minos_errors[mcl][self.parameter_names[i]] = {'interval': [-error_array[i], error_array[i]], + 'number': i, + 'name': self.parameter_names[i], + 'is_valid': self.m_binned.valid} + #raise e + + else: + self.hesse_errors = {0.683: {}} + for i in range(len(self.parameter_names)): + self.hesse_errors[0.683][self.parameter_names[i]] = {'interval': [-error_array[i], error_array[i]], + 'number': i, + 'name': self.parameter_names[i], + 'is_valid': self.m_binned.valid} if self.print_level == 1: - logger.info('Fit results: {}'.format(result_array)) - logger.info('Errors: {}'.format(error_array)) + logger.info(self.m_binned.fmin) + #logger.info('Correlation matrix: {}'.format(self.m_binned.covariance.correlation())) return result_array, error_array @@ -165,18 +260,44 @@ def PoissonLogLikelihood(self, params): expectation = model_return if np.min(expectation) < 0: - logger.error('Expectation contains negative numbers: Minimum {} -> {}. They will be excluded but something could be horribly wrong.'.format(np.argmin(expectation), np.min(expectation))) + logger.error('Expectation contains negative numbers: Minimum {} -> {}.'.format(np.argmin(expectation), np.min(expectation))) logger.error('FYI, the parameters are: {}'.format(params)) + logger.info('Expectation is: {}'.format(expectation)) + import matplotlib.pyplot as plt + plt.figure() + plt.step(self.bin_centers, self.hist) + plt.plot(self.bin_centers, expectation) + + #raise ValueError('Expecation below zero') + + logger.error("try again") + # expectation + model_return = self.model(self.bin_centers, *params) + if np.shape(model_return)[0] == 2: + expectation, expectation_error = model_return + else: + expectation = model_return + plt.plot(self.bin_centers, expectation,linestyle='--') + plt.savefig('negative_expectation.png', dpi=200) # exclude bins where expectation is <= zero or nan - index = np.where(expectation>0) + index = np.where(expectation>0)#np.where(expectation>0)#np.finfo(0.0).resolution) + if "background" in self.parameter_names and self.m_binned.fixed["background"]: + endpoint_parameter = self.parameter_names.index('endpoint') + index = np.where((self.bin_centers+0.5*(self.bin_centers[1]-self.bin_centers[0])<=params[endpoint_parameter]) & + (self.hist>=5)) + #index = np.arange(np.min(np.where(self.hist>1)[0]), np.max(np.where(self.hist>1)[0])+1) + #print(self.hist[index]) + #print(self.bin_centers[index]) # poisson log likelihoood - ll = (self.hist[index]*np.log(expectation[index]) - expectation[index]).sum() + log_factorial = np.array([np.sum(np.log(np.arange(1, n+1))) for n in self.hist[index]]) + ll = (self.hist[index]*np.log(expectation[index]) - expectation[index]-log_factorial).sum() # extended ll: poisson total number of events N = np.nansum(expectation) - extended_ll = -N+np.sum(self.hist)*np.log(N)+ll + log_factorial = np.sum(np.log(np.arange(1, np.sum(self.hist)+1))) + extended_ll = -N+np.sum(self.hist)*np.log(N)+ll-log_factorial return extended_ll @@ -188,6 +309,41 @@ def negPoissonLogLikelihood(self, params): # constrained parameters if len(self.constrained_parameters) > 0: for i, param in enumerate(self.constrained_parameters): - neg_ll += 0.5 * ((params[param] - self.constrained_means[i])/ self.constrained_widths[i])**2 + # only do uncorrelated parameters + if not param in self.correlated_parameters: + neg_ll += 0.5 * ((params[param] - self.constrained_means[i])/ self.constrained_widths[i])**2 + 0.5*np.log(2*np.pi) + np.log(self.constrained_widths[i]) + + constrained_indices = np.in1d(self.constrained_parameters, self.correlated_parameters).nonzero()[0] + if len(constrained_indices) > 0: + dim = len(self.correlated_parameters) + constrained_indices = np.in1d(self.constrained_parameters, self.correlated_parameters).nonzero()[0] + param_indices = self.correlated_parameters + neg_ll += 0.5*(np.log(np.linalg.det(self.cov_matrix)) + \ + np.dot(np.transpose(np.subtract(params[param_indices], np.array(self.constrained_means)[constrained_indices])), \ + np.dot(np.linalg.inv(self.cov_matrix), np.subtract(params[param_indices], np.array(self.constrained_means)[constrained_indices]))) + \ + dim*np.log(2*np.pi)) return neg_ll + + + def leastSquares(self, params): + + # expectation + model_return = self.model(self.bin_centers, *params) + if np.shape(model_return)[0] == 2: + expectation, expectation_error = model_return + else: + expectation = model_return + + nonzero_bins_index = np.where(self.hist>0)#np.where(expectation>0)#np.finfo(0.0).resolution) + zero_bins_index = np.where(self.hist==0) + index = np.where(expectation>0) + if "background" in self.parameter_names and self.m_binned.fixed["background"]: + # endpoint_parameter = self.parameter_names.index('endpoint') + index = np.where((expectation>1) & (self.hist>1)) + total_chisquare, _ = chisquare(self.hist[index], expectation[index]) + #chi2 = np.nansum((self.hist[nonzero_bins_index]-expectation[nonzero_bins_index])**2/expectation[nonzero_bins_index]) + #chi2+= np.nansum((self.hist[zero_bins_index]-expectation[zero_bins_index])**2) + #chi2 = 2*((expectation - self.hist + self.hist*np.log(self.hist/expectation))[nonzero_bins_index]).sum() + #chi2 += 2*(expectation - self.hist)[zero_bins_index].sum() + return total_chisquare #LeastSquares(self.bin_centers, self.hist, np.sqrt(self.hist), self.model) \ No newline at end of file diff --git a/mermithid/processors/Fitters/MCUncertaintyPropagation.py b/mermithid/processors/Fitters/MCUncertaintyPropagation.py new file mode 100644 index 00000000..e936476e --- /dev/null +++ b/mermithid/processors/Fitters/MCUncertaintyPropagation.py @@ -0,0 +1,182 @@ +''' +Author: C. Claessens +Date:8/2/2021 +Description: + + +''' + +from __future__ import absolute_import + +import numpy as np +from copy import deepcopy + +from morpho.utilities import morphologging, reader +from morpho.processors import BaseProcessor + +logger = morphologging.getLogger(__name__) + + + +__all__ = [] +__all__.append(__name__) + +class MCUncertaintyPropagation(BaseProcessor): + ''' + Processor that + Args: + + Inputs: + data: + Output: + result: dictionary containing fit results and uncertainties + ''' + def InternalConfigure(self, params): + ''' + Configure + ''' + + self.model = reader.read_param(params, 'model', "required") + self.fit = reader.read_param(params, 'fit_function', "required") + self.gen_and_fit = reader.read_param(params, 'gen_and_fit_function', "required") + self.fit_config_dict = deepcopy(reader.read_param(params, 'fit_config_dict', "required")) + self.fit_options = reader.read_param(params, 'fit_options', "optional") + self.sample_parameters = reader.read_param(params, 'sample_parameters', []) + self.stat_sys_combined = reader.read_param(params, 'stat_sys_combined', [True, True, True]) + self.N = reader.read_param(params, 'N_samples', 50) + self.fitted_params = reader.read_param(params, "initial_best_fit", []) + self.fitted_params_errors = reader.read_param(params, "initial_best_fit_errors", []) + self.Counts = reader.read_param(params, "Counts", 0) + + return True + + def InternalRun(self): + + self.results = {} + + for k in self.fit_options.keys(): + self.fit_config_dict[k] = self.fit_options[k] + + if len(self.fitted_params) == 0 or len(self.fitted_params_errors) == 0 or self.Counts == 0: + self.InitialFit() + self.ParameterSampling() + + + #self.results['best_fit'] = list(self.fitted_params) + + return True + + def InitialFit(self): + ''' + Fit data with best model + + Returns + ------- + None. + + ''' + + fit_successful = False + counter = 0 + self.fit_config_dict['print_level'] = 1 + #while (not fit_successful) and counter < 15: + # counter += 1 + # try: + + self.fitted_params, self.fitted_params_errors, self.Counts = self.fit(self.data, + self.fit_config_dict) + fit_successful = True + self.fit_config_dict['print_level'] = 0 + # except Exception as e: + # print(e) + # logger.error('Repeating fit') + # continue + + + logger.info('Best fit: {}'.format(self.fitted_params)) + #x, pdf, bins, fitted_model, asimov_data = self.model(self.fit_config_dict, + # params=self.fitted_params) + return True + + def ParameterSampling(self): + + start_j = 0 + + # propagate errors by sampling events and prior + return_dict_keys = ['stat', 'sys', 'combined'] + parameter_sampling = [{}, self.sample_parameters, self.sample_parameters] + + + for k_i, k in enumerate(return_dict_keys): + if self.stat_sys_combined[k_i]: + + if k == 'sys': + fixed_data = ['asimov'] + elif k == 'combined': + fixed_data = [] + else: + fixed_data = [] + + + Nparams = len(self.fitted_params) + offsets = np.zeros((Nparams, self.N)) + sigmas = np.zeros((Nparams, self.N)) + fit_results = np.zeros((Nparams, self.N)) + parameter_samples = [] + + + # sequential sampling + all_fit_returns = [] + for i in range(start_j, self.N): + fit_successful = False + counter = 0 + #while (not fit_successful) and counter < 5: + # counter += 1 + # try: + all_fit_returns.append(self.gen_and_fit(self.fitted_params, self.Counts, + self.fit_config_dict, + parameter_sampling[k_i], + i, fixed_data)) + # fit_successful = True + # except Exception as e: + # print(e) + # logger.error('Repeating fit') + # continue + + + + ################################### + # sort fit results + ################################### + #all_fit_returns = np.array(all_fit_returns) + for ii in range(len(all_fit_returns)): + j = ii + start_j + results = all_fit_returns[ii][0] + errors = all_fit_returns[ii][1] + parameter_samples.append(all_fit_returns[ii][2]) + + + for i in range(len(self.fitted_params)): + offsets[i][j] = results[i]-self.fitted_params[i] + sigmas[i][j] = errors[i] + fit_results[i][j] = results[i] + + + parameter_samples_transpose = {} + for p_key in self.sample_parameters.keys(): + parameter_samples_transpose[p_key] = [] + if len(parameter_samples) > 0 and p_key in parameter_samples[0].keys(): + for p_i in parameter_samples: + parameter_samples_transpose[p_key].append(p_i[p_key]) + + + self.results[k] = {'offsets': [list(o) for o in offsets], + 'sigmas': [list(s) for s in sigmas], + 'results': [list(fr) for fr in fit_results], + 'parameter_samples': parameter_samples_transpose, + 'best_fit': list(self.fitted_params), + 'best_fit_errors': list(self.fitted_params_errors) + } + + + diff --git a/mermithid/processors/Fitters/__init__.py b/mermithid/processors/Fitters/__init__.py index 634545ab..40268845 100644 --- a/mermithid/processors/Fitters/__init__.py +++ b/mermithid/processors/Fitters/__init__.py @@ -4,4 +4,5 @@ from __future__ import absolute_import from .BinnedDataFitter import BinnedDataFitter +from .MCUncertaintyPropagation import MCUncertaintyPropagation diff --git a/mermithid/processors/TritiumSpectrum/BinnedTritiumMLFitter.py b/mermithid/processors/TritiumSpectrum/BinnedTritiumMLFitter.py new file mode 100755 index 00000000..85e37249 --- /dev/null +++ b/mermithid/processors/TritiumSpectrum/BinnedTritiumMLFitter.py @@ -0,0 +1,1803 @@ + +""" +Author: C. Claessens +Date:8/03/2021 +Description: + Processor for analyzing tritium data with frequentist methods + + contains tritium model(s) + convolves with energy resolution + convolves with lineshape + multiplies with efficiency + adds background + uses BinnedDataFitter to fit binned spectrum with iminuit minimizer + + +""" + +import json +import os +from copy import deepcopy +import numpy as np +from scipy import constants +from scipy.special import erfc +from scipy.signal import convolve +import matplotlib.pyplot as plt + +from scipy.interpolate import interp1d + + +from morpho.utilities import morphologging, reader +logger = morphologging.getLogger(__name__) + +from mermithid.processors.Fitters import BinnedDataFitter +from mermithid.misc.FakeTritiumDataFunctions import fermi_func, pe, Ee, GF, Vud, Mnuc2 +from mermithid.processors.misc.KrComplexLineShape import KrComplexLineShape +from mermithid.misc.DetectionEfficiencyUtilities import pseudo_integrated_efficiency, integrated_efficiency, power_efficiency + + +#electron_mass = constants.electron_mass/constants.e*constants.c**2 +#FineStructureConstant = 0.0072973525664 + +# ============================================================================= +# Functions that can be imported from here and facitilate working with the processor +# ============================================================================= +# +# They create an instance of the processor defined below and use it for analysis +# GenAndFit tells the processor to generate new fake data and fit it +# DoOneFit give the processor data and tells it to fit it +# GetPDF returns model pdf for some parameters + + +def GenAndFit(params, counts, fit_config_dict, sampled_priors={}, + i=0, fixed_data = [], error_scaling=1, tilt = None, + fit_tilt = False, event=None): + + + if i%20 == 0: + logger.info('Sampling: {}'.format(i)) + + + T = BinnedTritiumMLFitter("TritiumFitter_{}".format(i)) + T.InternalConfigure(fit_config_dict) + #T.error_scaling = error_scaling + T.integrate_bins = True + + if tilt is not None:# and tilt !=0: + T.tilted_efficiency = True + T.tilt = tilt + + + # generate random data from best fit parameters + if len(fixed_data) == 0: + _, new_data = T.GenerateData(params, counts) + + else: + _, new_data = T.GenerateAsimovData(params) + + + if fit_tilt: + T.fix_tilt = False + + T.freq_data = new_data + results, errors = T.SampleConvertAndFit(sampled_priors, params) + parameter_samples = T.parameter_samples + + #logger.info('Fit results: {}'.format(results)) + + + if fit_config_dict['minos_intervals']: + return results, T.minos_errors, parameter_samples + else: + return results, errors, parameter_samples + + +def DoOneFit(data, fit_config_dict, sampled_parameters={}, error_scaling=0, + tilt=None, fit_tilt = False, data_is_energy=False): + + + T = BinnedTritiumMLFitter("TritiumFitter") + T.InternalConfigure(fit_config_dict) + #T.print_level=1 + #T.error_scaling = error_scaling + T.integrate_bins = True + logger.info('Energy stepsize: {}'.format(T.denergy)) + + if tilt is not None:# and tilt != 0: + T.tilted_efficiency = True + T.tilt = tilt + + if fit_tilt: + logger.info('Going to fit efficiency tilt') + T.fix_tilt = False + + if data_is_energy: + data = T.Frequency(data) + T.freq_data = data + results, errors = T.SampleConvertAndFit(sampled_parameters) + total_counts = results[1]+results[3] + + if fit_config_dict['minos_intervals']: + print(T.m_binned.covariance.correlation()) + return results, T.minos_errors, total_counts + elif 'return_ll' in fit_config_dict and fit_config_dict['return_ll']: + + #modified_results = deepcopy(fit_config_dict['model_parameter_means']) + #modified_results[2] = results[2] + + # this works + modified_results = deepcopy(results) + modified_results[1] = 0 + + all_params = modified_results + #all_params = fit_config_dict['model_parameter_means'] + + + + results_best_mass = deepcopy(all_params) + results_best_mass[2] = max(0, results[2]) + results_true_mass = deepcopy(all_params) + results_true_mass[2] = fit_config_dict['model_parameter_means'][2] + print('True mass: ', fit_config_dict['model_parameter_means'][2]) + print('True all: ', fit_config_dict['model_parameter_means']) + # get likleihood from fit + T.hist = T.TritiumSpectrumBackground(T.bin_centers, *modified_results) + fit_ll = T.PoissonLogLikelihood(results_true_mass) + # get likleihood of asimov best mass + #T.hist = T.TritiumSpectrumBackground(T.bin_centers, *results_best_mass) + best_ll = T.PoissonLogLikelihood(results_best_mass) + + return results, T.hesse_errors, [fit_ll, best_ll] + else: + return results, T.hesse_errors, total_counts + + +def GetPDF(fit_config_dict, params, plot=False): + logger.info('Plotting lineshape: {}'.format(plot)) + logger.info('PDF for params: {}'.format(params)) + + + T = BinnedTritiumMLFitter("TritiumFitter") + T.InternalConfigure(fit_config_dict) + T.plot_lineshape = plot + T.pass_efficiency_error = True + + + pdf = T.TritiumSpectrumBackground(T.energies, *params) + _, asimov_binned_data = T.GenerateAsimovData(params) + binned_fit = T.TritiumSpectrumBackground(T.bin_centers, *params) + + return T.energies, pdf, T.bin_centers, binned_fit, asimov_binned_data + + +# ============================================================================= +# Processor definition +# ============================================================================= + +class BinnedTritiumMLFitter(BinnedDataFitter): + + + def InternalConfigure(self, config_dict): + + # ==================================== + # Model configuration + # ==================================== + self.use_approx_model = reader.read_param(config_dict, 'use_approximate_model', True) + self.use_toy_model_efficiency = reader.read_param(config_dict, 'use_toy_model_efficiency', False) + self.is_distorted = reader.read_param(config_dict, 'distorted', False) # multiply spectrum with efficency + self.is_smeared = reader.read_param(config_dict, 'smeared', True) # convolve with energy resolution + self.error_scaling = reader.read_param(config_dict, 'error_scaling', 1.) # scales efficiency uncertainty + self.is_scattered = reader.read_param(config_dict, 'scattered', False) # convolve with lineshape + self.integrate_bins = reader.read_param(config_dict, 'integrate_bins', True) # integrate spectrum over bin widths + self.fit_efficiency_tilt = reader.read_param(config_dict, 'fit_efficiency_tilt', False) # efficiency slope is free parameter + self.fit_nu_mass = reader.read_param(config_dict, 'fit_neutrino_mass', False) + self.use_relative_livetime_correction = reader.read_param(config_dict, 'use_relative_livetime_correction', False) + self.neg_mbeta_squared_model = reader.read_param(config_dict, 'neg_mbeta_squared_model', 'mainz') + + if self.fit_nu_mass: + logger.info('Using {} model'.format(self.neg_mbeta_squared_model)) + + # save plots in (processor can plot lineshape used in tritium model) + self.savepath = reader.read_param(config_dict, 'savepath', '.') + + # detector response options + self.NScatters = reader.read_param(config_dict, 'NScatters', 20) + self.resolution_model = reader.read_param(config_dict, 'resolution_model', 'gaussian') + self.lineshape_model = reader.read_param(config_dict, 'lineshape_model', 'simplified') + self.simplified_lineshape_path = reader.read_param(config_dict, 'simplified_lineshape_path', "required") + self.helium_lineshape_path = reader.read_param(config_dict, 'helium_lineshape_path', "optional") + self.use_helium_scattering = reader.read_param(config_dict, 'use_helium_scattering', False) + self.use_frequency_dependent_lineshape = reader.read_param(config_dict, 'use_frequency_dependent_lineshape', True) + self.correlated_p_q_res = reader.read_param(config_dict, 'correlated_p_q_res', False) + self.correlated_p_q = reader.read_param(config_dict, 'correlated_p_q', False) + self.derived_two_gaussian_model = reader.read_param(config_dict, 'derived_two_gaussian_model', True) + + + # configure model parameter names + self.model_parameter_names = reader.read_param(config_dict, 'model_parameter_names', + ['endpoint', 'background', 'm_beta_squared', 'Amplitude', + 'scatter_peak_ratio_p', 'scatter_peak_ratio_q', + 'resolution', 'two_gaussian_mu_1', 'two_gaussian_mu_2'] ) + # initial values and mean of constaints (if constraint) or mean of distribution (if sampled) + self.model_parameter_means = reader.read_param(config_dict, 'model_parameter_means', [18.6e3, 0, 0, 5000, 0.8, 1, 15, 0, 0]) + # width for constraints or sample distributions + self.model_parameter_widths = reader.read_param(config_dict, 'model_parameter_widths', [100, 0.1, 0.1, 500, 0.1, 0, 3, 1, 1]) + self.fixed_parameters = reader.read_param(config_dict, 'fixed_parameters', [False, False, False, False, True, True, True, True, True]) + self.fixed_parameter_dict = reader.read_param(config_dict, 'fixed_parameter_dict', {}) + self.free_in_second_fit = reader.read_param(config_dict, 'free_in_second_fit', []) + self.fixed_in_second_fit = reader.read_param(config_dict, 'fixed_in_second_fit', []) + self.limits = reader.read_param(config_dict, 'model_parameter_limits', + [[18e3, 20e3], + [0, None], + [-300**2, 300**2], + [100, None], + [0.1, 1.], + [0.1, 1.], + [12, 100], + [-100, 100], + [-100, 100]]) + self.error_def = reader.read_param(config_dict,'error_def', 0.5) + + # check that configuration is consistent + if (len(self.model_parameter_names) != len(self.model_parameter_means) or len(self.model_parameter_names) != len(self.model_parameter_widths) or len(self.model_parameter_names) != len(self.fixed_parameters)): + logger.error('Number of parameter names does not match other parameter configurations') + return False + + + # ==================================== + # Parameter configuration + # ==================================== + + self.parameter_samples = {} + + # need to know which parameter is background + self.background_index = self.model_parameter_names.index('background') + # and which one is B + # self.B_index = self.model_parameter_names.index('B') + # signal counts + self.amplitude_index = self.model_parameter_names.index('Amplitude') + + + # resolutions + + self.scale_mean = reader.read_param(config_dict, 'scale_mean', 1) + self.scale_width = reader.read_param(config_dict, 'scale_width', 0) + self.width_scaling = self.scale_mean + + if self.is_smeared: + if 'resolution' in self.model_parameter_names: + self.res_index = self.model_parameter_names.index('resolution') + + self.res_mean = reader.read_param(config_dict, 'gaussian_resolution_mean', 15.0) + self.res_width = reader.read_param(config_dict, 'gaussian_resolution_width', 1.0) + self.res_width_from_maxSNR = reader.read_param(config_dict, 'sigma_std_maxSNR', 0) + self.res = self.res_mean + + if self.resolution_model != 'gaussian': + self.two_gaussian_fraction = reader.read_param(config_dict, 'two_gaussian_fraction', 1.) + self.two_gaussian_mu_1 = reader.read_param(config_dict, 'two_gaussian_mu1', 0) + self.two_gaussian_mu_2 = reader.read_param(config_dict, 'two_gaussian_mu2', 0) + if 'two_gaussian_mu_1' in self.model_parameter_names: + self.two_gaussian_mu_1_index = self.model_parameter_names.index('two_gaussian_mu_1') + if 'two_gaussian_mu_2' in self.model_parameter_names: + self.two_gaussian_mu_2_index = self.model_parameter_names.index('two_gaussian_mu_2') + + if self.derived_two_gaussian_model: + self.two_gaussian_p0 = reader.read_param(config_dict, 'two_gaussian_p0', 1.) + self.two_gaussian_p1 = reader.read_param(config_dict, 'two_gaussian_p1', 1.) + else: + if 'two_gaussian_sigma_1' in self.model_parameter_names: + self.two_gaussian_sigma_1_index = self.model_parameter_names.index('two_gaussian_sigma_1') + if 'two_gaussian_sigma_2' in self.model_parameter_names: + self.two_gaussian_sigma_2_index = self.model_parameter_names.index('two_gaussian_sigma_2') + + + self.two_gaussian_sigma_1_mean = reader.read_param(config_dict, 'two_gaussian_sigma_1_mean', 15) + self.two_gaussian_sigma_2_mean = reader.read_param(config_dict, 'two_gaussian_sigma_2_mean', 5) + self.two_gaussian_sigma_1_width = reader.read_param(config_dict, 'two_gaussian_sigma_1_width', 1) + self.two_gaussian_sigma_2_width = reader.read_param(config_dict, 'two_gaussian_sigma_2_width', 1) + # initial setting + self.two_gaussian_sigma_1 = deepcopy(self.two_gaussian_sigma_1_mean) + self.two_gaussian_sigma_2 = deepcopy(self.two_gaussian_sigma_2_mean) + + + + + # scatter peak ratio + if self.is_scattered: + if 'scatter_peak_ratio_p' in self.model_parameter_names: + self.scatter_peak_ratio_p_index = self.model_parameter_names.index('scatter_peak_ratio_p') + if 'scatter_peak_ratio_q' in self.model_parameter_names: + self.scatter_peak_ratio_q_index = self.model_parameter_names.index('scatter_peak_ratio_q') + if 'h2_fraction' in self.model_parameter_names: + self.h2_fraction_index = self.model_parameter_names.index('h2_fraction') + + self.spr_factor = reader.read_param(config_dict, 'SPR_factor', 0) + self.scatter_peak_ratio_p_mean = reader.read_param(config_dict, 'scatter_peak_ratio_p_mean', 0.7) + self.scatter_peak_ratio_p_width = reader.read_param(config_dict, 'scatter_peak_ratio_p_width', 0.1) + self.scatter_peak_ratio_q_mean = reader.read_param(config_dict, 'scatter_peak_ratio_q_mean', 0.7) + self.scatter_peak_ratio_q_width = reader.read_param(config_dict, 'scatter_peak_ratio_q_width', 0.1) + self.h2_fraction_mean = reader.read_param(config_dict, 'h2_fraction_mean', 1.0) + self.h2_fraction_width = reader.read_param(config_dict, 'h2_fraction_width', 0.0) + + self.scatter_peak_ratio_mean = reader.read_param(config_dict, 'scatter_peak_ratio_mean', 0.5) + self.scatter_peak_ratio_width = reader.read_param(config_dict, 'scatter_peak_ratio_width', 0.1) + + self.scatter_peak_ratio_p = self.scatter_peak_ratio_p_mean + self.scatter_peak_ratio_q = self.scatter_peak_ratio_q_mean + self.h2_fraction = self.h2_fraction_mean + + #Adding correlated parameters (will later incorporate into morpho processor) + self.p_q_corr = reader.read_param(config_dict, 'p_q_corr', 0) + self.p_scale_corr = reader.read_param(config_dict, 'p_scale_corr', 0) + self.q_scale_corr = reader.read_param(config_dict, 'q_scale_corr', 0) + self.p_q_max_snr_stds = reader.read_param(config_dict, 'p_q_max_snr_stds', [0, 0]) + + + stds = [self.scatter_peak_ratio_p_width, self.scatter_peak_ratio_q_width, self.res_width] + self.p_q_res_cov_matrix = [[stds[0]**2, self.p_q_corr*stds[0]*stds[1], self.p_scale_corr*self.p_q_max_snr_stds[0]*self.res_width_from_maxSNR], + [self.p_q_corr*stds[0]*stds[1], stds[1]**2, self.q_scale_corr*self.p_q_max_snr_stds[1]*self.res_width_from_maxSNR], + [self.p_scale_corr*self.p_q_max_snr_stds[0]*self.res_width_from_maxSNR, self.q_scale_corr*self.p_q_max_snr_stds[1]*self.res_width_from_maxSNR, stds[2]**2]] + + self.p_q_cov_matrix = [[stds[0]**2, self.p_q_corr*stds[0]*stds[1]], + [self.p_q_corr*stds[1]*stds[0], stds[1]**2]] + + + + + # identify tritium parameters + self.tritium_model_indices = reader.read_param(config_dict, 'tritium_model_parameters', [0, 2]) + self.m_beta_index = self.model_parameter_names.index('m_beta_squared') + self.endpoint_index = self.model_parameter_names.index('endpoint') + if 'B' in self.model_parameter_names: + self.B_index = self.model_parameter_names.index('B') + self.fixed_parameters[self.m_beta_index] = not self.fit_nu_mass + self.endpoint=reader.read_param(config_dict, 'true_endpoint', 18.573e3) + #logger.info('Tritium model parameters: {}'.format(np.array(self.model_parameter_names)[self.tritium_model_indices])) + + + # final state spectrum + self.use_final_states = reader.read_param(config_dict, 'use_final_states', False) + self.final_state_array = reader.read_param(config_dict, 'final_states_array', [[0], [1]]) + + #self.max_final_state_energy_loss = np.max(np.abs(self.final_state_array[0])) + #self.final_states_interp = interp1d(self.final_state_array[0]-np.max(self.final_state_array[0]), self.final_state_array[1], bounds_error=False, fill_value=0) + #max_energy = self.max_final_state_energy_loss + #dE = 1 + #n_dE = round(max_energy/dE) + #e_lineshape = np.arange(-n_dE*dE, n_dE*dE, dE) + #plt.figure() + #plt.plot(e_lineshape, self.final_states_interp(e_lineshape)) + #plt.yscale('log') + #plt.savefig(os.path.join(self.savepath,'final_states_interp.png'), dpi=300) + + + + + + # individual model parameter configurations + # overwrites model_parameter_means and widths + self.B_mean = reader.read_param(config_dict, 'B_mean', 1) + self.B_width = reader.read_param(config_dict, 'B_width', 1e-6) + self.B = self.B_mean + + self.endpoint_mean = reader.read_param(config_dict,'endpoint_mean', self.model_parameter_means[self.endpoint_index]) + self.endpoint_width = reader.read_param(config_dict, 'endpoint_width', self.model_parameter_widths[self.endpoint_index]) + + # frequency an energy range + self.min_frequency = reader.read_param(config_dict, 'min_frequency', "required") + self.max_frequency = reader.read_param(config_dict, 'max_frequency', None) + self.max_energy = reader.read_param(config_dict, 'resolution_energy_max', 1000) + + # path to json with efficiency dictionary + self.efficiency_file_path = reader.read_param(config_dict, 'efficiency_file_path', '') + + # channel livetimes + self.channel_transition_freqs = np.array(reader.read_param(config_dict, 'channel_transition_freqs', [[0,1.38623121e9+24.5e9], + [1.38623121e9+24.5e9, 1.44560621e9+24.5e9], + [1.44560621e9+24.5e9, 50e9]])) + self.channel_livetimes = reader.read_param(config_dict, 'channel_livetimes', [7185228, 7129663, 7160533]) + self.channel_relative_livetimes = np.array(self.channel_livetimes)/ np.max(self.channel_livetimes) + + # freqeuncy dependent detector response + self.ins_res_bounds_freq = reader.read_param(config_dict, 'ins_res_bounds_freq', []) + self.scatter_factor_bounds = self.Energy(self.ins_res_bounds_freq) + self.p_factors = np.array(reader.read_param(config_dict, 'p_factors', [1])) + self.q_factors = np.array(reader.read_param(config_dict, 'q_factors', [1])) + self.p_factors_mean = np.array(reader.read_param(config_dict, 'p_factors_mean', [1])) + self.q_factors_mean = np.array(reader.read_param(config_dict, 'q_factors_mean', [1])) + self.p_factors_width = np.array(reader.read_param(config_dict, 'p_factors_width', [1])) + self.q_factors_width = np.array(reader.read_param(config_dict, 'q_factors_width', [1])) + self.width_factors = np.array(reader.read_param(config_dict, 'res_width_factors', [1])) + + #self.scatter_factor_bounds = np.sort(self.Energy([25.90*10**9, 25.91*10**9, 25.92*10**9, 25.925*10**9, 25.93*10**9, 25.94*10**9, 25.96*10**9, 25.965*10**9, 25.9675*10**9, 25.968*10**9, 25.97*10**9, 25.98*10**9])) + #self.p_factors = np.flip([0.9983968696582269, 1.0241234738781622, 1.074069218435362, 1.1880615082443595, 0.7630526168402478, 0.9252229133675792, 1.0099495204807443, 1.0305682175456141, 1.754449151085981, 1.0427487909986666, 1.0074967873980396, 0.8585656489155274, 0.9830120037530994]) + #self.q_factors = np.flip([0.9961848753897812, 0.9911348282030333, 1.003671359578592, 0.9575276167823439, 1.022755464502686, 1.0139235105836255, 0.9964227505579599, 0.9854455059345174, 0.9613799082228937, 1.0068195698448816, 0.9995181762199474, 1.0273166210271425, 1.0054718471162767]) + + + + # ================= + # Fit configuration + # ================= + + self.print_level = reader.read_param(config_dict, 'print_level', 0) + #self.fix_nu_mass = not self.fit_nu_mass + self.pass_efficiency_error = False + self.use_asimov = False + #self.fix_endpoint = not reader.read_param(config_dict, 'fit_endpoint', True) + #self.fix_background = not reader.read_param(config_dict, 'fit_background', True) + #self.fix_amplitude = not reader.read_param(config_dict, 'fit_amplitude', True) + #self.counts_guess = reader.read_param(config_dict, 'counts_guess', 5000) + #self.mass_guess = reader.read_param(config_dict, 'nu_mass_guess', 0.0) + + + + + # Parameters can be constrained manually or by inlcuding them in the nuisance parameter dictionary + #self.constrained_parameter_names = reader.read_param(config_dict, 'constrained_parameter_names', []) + self.constrained_parameters = reader.read_param(config_dict, 'constrained_parameters', []) + self.constrained_means = np.array(self.model_parameter_means)[self.constrained_parameters]#reader.read_param(config_dict, 'constrained_means', []) + self.constrained_widths = np.array(self.model_parameter_widths)[self.constrained_parameters]#reader.read_param(config_dict, 'constrained_widths', []) + + + self.nuisance_parameters = reader.read_param(config_dict, 'nuisance_parameters', {}) + + for i, p in enumerate(self.model_parameter_names): + if p in self.nuisance_parameters.keys(): + if self.nuisance_parameters[p]: + self.constrained_parameters.append(i) + self.constrained_means = np.append(self.constrained_means, self.model_parameter_means[i]) + self.constrained_widths = np.append(self.constrained_widths, self.model_parameter_widths[i]) + if i in self.constrained_parameters: + self.fixed_parameters[i] = False + + #logger.info('Here comes the covariance matrix') + if self.is_scattered and self.correlated_p_q_res: + self.correlated_parameters = [self.scatter_peak_ratio_p_index, self.scatter_peak_ratio_q_index, self.res_index] + self.cov_matrix = self.p_q_res_cov_matrix + #logger.info(self.cov_matrix) + elif self.is_scattered and self.correlated_p_q: + self.correlated_parameters = [self.scatter_peak_ratio_p_index, self.scatter_peak_ratio_q_index] + self.cov_matrix = self.p_q_cov_matrix + #logger.info(self.cov_matrix) + else: + self.correlated_parameters = [] + + + + # MC uncertainty propagation does not need the fit uncertainties returned by iminuit. uncertainties are instead obtained from the distribution of fit results. + # But if the uncertainty is unstead propagated by adding constraiend nuisance parameters then the fit uncertainties are needed. + # imnuit can calculated hesse and minos intervals. the former are symmetric. we want the asymetric intrevals-> minos + # minos_cls is the list of uncertainty level that should be obtained: e.g. [0.68, 0.9] + self.minos_intervals = reader.read_param(config_dict, 'minos_intervals', False) + self.minos_cls = reader.read_param(config_dict, 'minos_confidence_levels', [0.683]) + + + + # ================================== + # detection efficiency configuration + # ================================== + + self.tilted_efficiency = False + self.tilt = 0. + + if self.use_toy_model_efficiency: + x = np.linspace(self.min_frequency, self.max_frequency, 100) + y = power_efficiency(x, plot=False) + + self.power_eff_interp = interp1d(x, y[0], fill_value=0, bounds_error=False) + self.power_eff_error_interp = interp1d(x, y[1][0], fill_value=1, bounds_error=False) + + elif self.efficiency_file_path != '': + with open(self.efficiency_file_path, 'r') as infile: + snr_efficiency_dict = json.load(infile) + + # don't ask ... it's complicated + snr_efficiency_dict['frequency'] = snr_efficiency_dict['frequencies'] + snr_efficiency_dict['good_fit_index'] = [True]*len((snr_efficiency_dict['frequencies'])) + snr_efficiency_dict['tritium_rates'] = snr_efficiency_dict['eff interp with slope correction'] + snr_efficiency_dict['tritium_rates_error'] = snr_efficiency_dict['error interp with slope correction'] + #snr_efficiency_dict['tritium_rates'] = snr_efficiency_dict['eff interp no energy correction'] + #snr_efficiency_dict['tritium_rates_error'] = snr_efficiency_dict['error interp no energy correction'] + + if self.max_frequency == None: + self.max_frequency = np.max(snr_efficiency_dict['frequency']) + + self.snr_efficiency_dict = snr_efficiency_dict + else: + self.efficiency_file_path = '' + + if self.max_frequency == None: + raise ValueError('Max frequency undetermined') + + # ================================== + # lineshape configuration + # ================================== + + # simplified lineshape parameters + self.lineshape_p = np.loadtxt(self.simplified_lineshape_path, unpack=True) + + # if true lineshape is plotted during tritium spectrum shape generation (for debugging) + self.plot_lineshape = False + + # helium lineshape + if self.use_helium_scattering: + self.helium_lineshape_p = np.loadtxt(self.helium_lineshape_path, unpack=True) + + # which lineshape should be used? + if self.lineshape_model == 'simplified': + self.multi_gas_lineshape = self.simplified_multi_gas_lineshape + elif self.lineshape_model == 'accurate_simplified': + self.multi_gas_lineshape = self.more_accurate_simplified_multi_gas_lineshape + + elif self.lineshape_model =='detailed': + self.detailed_scatter_spectra_path = reader.read_param(config_dict, 'detailed_lineshape_path') + + ## lineshape params + #self.SimpParams = [self.res*2*np.sqrt(2*np.log(2)), self.scatter_ratio] + + # Setup and configure lineshape processor + complexLineShape_config = { + 'gases': ["H2","He"], + 'max_scatters': self.NScatters, + 'fix_scatter_proportion': True, + # When fix_scatter_proportion is True, set the scatter proportion for gas1 below + 'gas1_scatter_proportion': self.h2_fraction, + # This is an important parameter which determines how finely resolved + # the scatter calculations are. 10000 seems to produce a stable fit with minimal slowdown, for ~4000 fake events. The parameter may need to + # be increased for larger datasets. + 'num_points_in_std_array': 10000, + 'B_field': self.B, + 'base_shape': 'dirac', + 'path_to_osc_strengths_files': self.detailed_scatter_spectra_path + } + logger.info('Setting up complex lineshape object') + self.complexLineShape = KrComplexLineShape("complexLineShape") + logger.info('Configuring complex lineshape') + self.complexLineShape.Configure(complexLineShape_config) + logger.info('Checking existence of scatter spectra files') + self.complexLineShape.check_existence_of_scatter_file() + + self.multi_gas_lineshape = self.complex_lineshape + else: + logger.error('Unknown configure lineshape') + return False + + # scatter peak ratio + self.scatter_peak_ratio = reader.read_param(config_dict, 'scatter_peak_ratio', 'modified_exponential') + + if self.scatter_peak_ratio == 'constant': + self.use_fixed_scatter_peak_ratio = True + elif self.scatter_peak_ratio == 'modified_exponential': + self.use_fixed_scatter_peak_ratio = False + + else: + logger.error("Configuration of scatter_peak_ratio not known. Options are 'constant' and 'modified_exponential") + raise ValueError("Configuration of scatter_peak_ratio not known. Options are 'constant' and 'efficiency_model") + + + + + # ================================== + # energies and bins + # ================================== + + # bin width used for poisson statistics fit + # energy stepsize is used for integration approximation: + # counts in a bin are integrated spectrum over bin width. + # integration is approximated by summing over a number of discrete steps in a bin. + self.dbins = reader.read_param(config_dict, 'energy_bin_width', 50) + self.denergy = reader.read_param(config_dict, 'energy_step_size', min([np.round(self.dbins/10, 2), 1])) + + # bin size has to divide energy step size + self.N_bins = np.round((self.Energy(self.min_frequency)-self.Energy(self.max_frequency))/self.dbins) + self.N_energy_bins = self.N_bins*np.round(self.dbins/self.denergy) + + # adjust energy stepsize to match bin division + self.denergy = self.dbins/np.round(self.dbins/self.denergy) + + + self._energies = np.arange(self.Energy(self.max_frequency), self.Energy(self.max_frequency)+(self.N_energy_bins)*self.denergy, self.denergy) + self._bins = np.arange(np.min(self.energies), self.Energy(self.max_frequency)+(self.N_bins)*self.dbins, self.dbins) + + if len(self._energies) > self.N_energy_bins: + self._energies = self._energies[:-1] + if len(self._bins) > self.N_bins: + self._bins = self._bins[:-1] + + + self.bin_centers = self._bins[0:-1]+0.5*(self._bins[1]-self._bins[0]) + self.freq_bins = self.Frequency(self._bins) + self.freq_bin_centers = self.Frequency(self.bin_centers) + + # arrays for internal usage + self._bin_efficiency, self._bin_efficiency_errors = [], [] + self._full_efficiency, self._full_efficiency_errors = [], [] + self._bin_efficiency, self._bin_efficiency_errors = self.Efficiency(self.bin_centers) + self._full_efficiency, self._full_efficiency_errors = self.Efficiency(self.energies) + + self.ReSetBins() + # ================================== + # configure parent BinnedDataFitter + # ================================== + + # This processor inherits from the BinnedDataFitter that does binned max likelihood fitting + # overwrite parent model with tritium model used here + self.model = self.TritiumSpectrumBackground + + # now configure fit + self.ConfigureFit() + + + return True + + + def ConfigureFit(self): + # configure fit + + # this should not be hard coded + #neutrino_limits = [-400**2, 400**2] + #energy_limits = [max([self.endpoint-1000, np.min(self.energies)+np.sqrt(neutrino_limits[1])]), min([self.endpoint+1000, np.max(self.energies)-np.sqrt(np.abs(neutrino_limits[0]))])] + #if self.print_level == 1: + # logger.warning('Neutrino mass limited to: {} - {}'.format(*np.sqrt(np.array(neutrino_limits)))) + + + #if not self.fit_efficiency_tilt: + self.parameter_names = self.model_parameter_names #['Endpoint', 'Background', 'm_beta_squared', 'Amplitude', + # 'scatter_peak_ratio_p', 'scatter_peak_ratio_q', + # 'res', 'two_gaussia_sigma_1', 'two_gaussian_sigma_2'] + self.initial_values = self.model_parameter_means#[self.endpoint, self.background, self.mass_guess**2, self.counts_guess, + #self.scatter_peak_ratio_p, self.scatter_peak_ratio_q, + #self.res, self.two_gaussian_sigma_1, self.two_gaussian_sigma_2] + + self.fixes = self.fixed_parameters #[self.fix_endpoint, self.fix_background, self.fix_nu_mass, self.fix_amplitude, + #self.fix_scatter_peak_ratio_p, self.fix_scatter_peak_ratio_q, + #self.fix_res, self.fix_two_gaussian_sigma_1, self.fix_two_gaussian_sigma_2] + + self.fixes_dict = self.fixed_parameter_dict + + # self.limits = [energy_limits, + # [0, None], + # neutrino_limits, + # [100, None], + # [0.1, 1.], + # [0.1, 1.], + # [30, 100], + # [1, 100], + # [1, 100]] + + + + self.parameter_errors = [max([0.1, p]) for p in self.model_parameter_widths] + + + + # else: + # logger.warning('Efficiency tilt will be fitted') + # self.tilted_efficiency = True + # self.parameter_names = ['Endpoint', 'Background', 'm_beta_squared', 'Amplitude', 'scatter_peak_ratio_p', 'scatter_peak_ratio_q', 'Efficiency tilt'] + # self.initial_values = [self.endpoint, 1, self.mass_guess**2, self.counts_guess, self.scatter_peak_ratio_p, self.scatter_peak_ratio_q, self.tilt] + # self.parameter_errors = [max([0.1, 0.1*p]) for p in self.initial_values] + # self.fixes = [self.fix_endpoint, self.fix_background, self.fix_nu_mass, self.fix_amplitude, self.fix_scatter_ratio_b, self.fix_scatter_ratio_c, self.fix_tilt] + # self.limits = [energy_limits, + # [1e-10, None], + # neutrino_limits, + # [0, None], + # [0.1, 1.], + # [0.1, 1.], + # [-0.5, 0.5]] + + + # if self.plot_lineshape: + # logger.info('Parameters: {}'.format(self.parameter_names)) + # logger.info('Fixed: {}'.format(self.fixes)) + # logger.info('Initial values: {}'.format(self.initial_values)) + + + return True + + # ========================================================================= + # Main fit function + # ========================================================================= + # This is the main function that is called from outside (besides Configure). + # Maybe I should rename it to InternalRun to match mermithid naming scheme (and make the processor an actual processor) + + def SampleConvertAndFit(self, sampled_parameters={}, params= []): + + + # for systematic MC uncertainty propagation: Generate Asimov data to fit with random model + if self.use_asimov: + #temp = self.error_scaling + #self.error_scaling = 0 + #self._bin_efficiency, self._bin_efficiency_error = self.Efficiency(self.bin_centers, pseudo=True) + if 'frequency_dependent_response_in_asimov' in sampled_parameters.keys() and sampled_parameters['frequency_dependent_response_in_asimov']: + self.use_frequency_dependent_lineshape = True + self.SampleFrequencyDependence() + self.hist = self.TritiumSpectrumBackground(self.bin_centers, *params) + self.use_frequency_dependent_lineshape = False + else: + self.hist = self.TritiumSpectrumBackground(self.bin_centers, *params) + #self.error_scaling = temp + + # sample priors that are to be sampled + if len(sampled_parameters.keys()) > 0: + self.SamplePriors(sampled_parameters) + + + # need to first re-calcualte energy bins with sampled B before getting efficiency + self.ReSetBins() + + if 'efficiency' in sampled_parameters.keys(): + random_efficiency = True + else: + random_efficiency = False + + # re-calculate bin efficiencies, if self.pseudo_eff=True efficiency will be ranomized + self._bin_efficiency, self._bin_efficiency_error = self.Efficiency(self.bin_centers, pseudo=random_efficiency) + + + # now convert frequency data to energy data and histogram it + if not self.use_asimov: + self.ConvertAndHistogram() + + # Call parent fit method using data and model from this instance + return self.fit() + + + # ========================================================================= + # Get random sample from normal, beta, or gamma distribution + # ========================================================================= + def Gaussian_sample(self, mean, width): + np.random.seed() + if isinstance(width, list): + return np.random.randn(len(width))*width+mean + else: + return np.random.randn()*width+mean + + def Beta_sample(self, mean, width): + np.random.seed() + a = ((1-mean)/(width**2)-1/mean)*mean**2 + b = (1/mean-1)*a + return np.random.beta(a, b) + + def Gamma_sample(self, mean, width): + np.random.seed() + a = (mean/width)**2 + b = mean/(width**2) + return np.random.gamma(a, 1/b) + + + def SamplePriors(self, sampled_parameters): + + for k in sampled_parameters.keys(): + if k in self.nuisance_parameters and self.nuisance_parameters[k] and sampled_parameters[k]: + raise ValueError('{} is nuisance parameter.'.format(k)) + + for i, p in enumerate(self.model_parameter_names): + if p in sampled_parameters.keys() and sampled_parameters[p] and not self.fixed_parameters[i]: + raise ValueError('{} is a free parameter'.format(p)) + + logger.info('Sampling: {}'.format([k for k in sampled_parameters.keys() if sampled_parameters[k]])) + self.parameter_samples = {} + sample_values = [] + if 'resolution' in sampled_parameters.keys() and sampled_parameters['resolution']: + self.res = self.Gaussian_sample(self.res_mean, self.res_width) + #if self.res <= 30.01/float(2*np.sqrt(2*np.log(2))): + # logger.warning('Sampled resolution small. Setting to {}'.format(30.01/float(2*np.sqrt(2*np.log(2))))) + # self.res = 30.01/float(2*np.sqrt(2*np.log(2))) + self.parameter_samples['resolution'] = self.res + sample_values.append(self.res) + if 'h2_fraction' in sampled_parameters.keys() and sampled_parameters['h2_fraction']: + self.h2_fraction = self.Gaussian_sample(self.h2_fraction_mean, self.h2_fraction_width) + if self.h2_fraction > 1: self.h2_fraction=1 + elif self.h2_fraction < 0: self.h2_fraction=0 + self.parameter_samples['h2_fraction'] = self.h2_fraction + sample_values.append(self.h2_fraction) + if 'two_gaussian_sigma_1' in sampled_parameters.keys() and sampled_parameters['two_gaussian_sigma_1']: + self.two_gaussian_sigma_1 = self.Gaussian_sample(self.two_gaussian_sigma_1_mean, self.two_gaussian_sigma_1_width) + self.parameter_samples['two_gaussian_sigma_1'] = self.two_gaussian_sigma_1 + sample_values.append(self.two_gaussian_sigma_1) + if 'two_gaussian_sigma_2' in sampled_parameters.keys() and sampled_parameters['two_gaussian_sigma_2']: + self.two_gaussian_sigma_2 = self.Gaussian_sample(self.two_gaussian_sigma_2_mean, self.two_gaussian_sigma_2_width) + self.parameter_samples['two_gaussian_sigma_2'] = self.two_gaussian_sigma_2 + sample_values.append(self.two_gaussian_sigma_2) + if 'scatter_peak_ratio' in sampled_parameters.keys() and sampled_parameters['scatter_peak_ratio']: + self.scatter_peak_ratio_p = self.Beta_sample(self.scatter_peak_ratio_mean, self.scatter_peak_ratio_width) + self.scatter_peak_ratio_q = 1 + #self.fix_scatter_ratio_b = True + #self.fix_scatter_ratio_c = True + self.parameter_samples['scatter_peak_ratio'] = self.scatter_peak_ratio_p + sample_values.append(self.scatter_peak_ratio_p) + + if self.correlated_p_q and 'scatter_peak_ratio_p' in sampled_parameters.keys() and 'scatter_peak_ratio_q' in sampled_parameters.keys() and sampled_parameters['scatter_peak_ratio_p'] and sampled_parameters['scatter_peak_ratio_q']: + logger.info('Correlated p, q, scale sampling') + correlated_vars = np.random.multivariate_normal([self.scatter_peak_ratio_p_mean, self.scatter_peak_ratio_q_mean], self.p_q_cov_matrix) + self.scatter_peak_ratio_p = correlated_vars[0] + self.scatter_peak_ratio_q = correlated_vars[1] + #self.width_scaling = correlated_vars[2] + + #self.fix_scatter_ratio_b = True + self.parameter_samples['scatter_peak_ratio_p'] = self.scatter_peak_ratio_p + sample_values.append(self.scatter_peak_ratio_p) + + self.parameter_samples['scatter_peak_ratio_q'] = self.scatter_peak_ratio_q + sample_values.append(self.scatter_peak_ratio_q) + #self.fix_scatter_ratio_c = True + + else: + + if 'scatter_peak_ratio_p' in sampled_parameters.keys() and sampled_parameters['scatter_peak_ratio_p']: + logger.info('Uncorrelated b, c, scale sampling') + self.scatter_peak_ratio_p = self.Gamma_sample(self.scatter_peak_ratio_p_mean, self.scatter_peak_ratio_p_width) + #self.fix_scatter_ratio_b = True + self.parameter_samples['scatter_peak_ratio_p'] = self.scatter_peak_ratio_p + sample_values.append(self.scatter_peak_ratio_p) + if 'scatter_peak_ratio_q' in sampled_parameters.keys() and sampled_parameters['scatter_peak_ratio_q']: + self.scatter_peak_ratio_q = self.Gamma_sample(self.scatter_peak_ratio_q_mean, self.scatter_peak_ratio_q_width) + self.parameter_samples['scatter_peak_ratio_q'] = self.scatter_peak_ratio_q + sample_values.append(self.scatter_peak_ratio_q) + #self.fix_scatter_ratio_c = True + + if 'B' in sampled_parameters.keys() and sampled_parameters['B']: + self.B = self.Gaussian_sample(self.B_mean, self.B_width) + self.parameter_samples['B'] = self.B + sample_values.append(self.B) + logger.info('B field prior: {} +/- {}'.format(self.B_mean, self.B_width)) + if 'endpoint' in sampled_parameters.keys() and sampled_parameters['endpoint']: + self.model_parameter_means[self.endpoint_index] = self.Gaussian_sample(self.endpoint_mean, self.endpoint_width) + self.parameter_samples['endpoint'] = self.endpoint + self.fix_endpoint = True + sample_values.append(self.endpoint) + if 'frequency_dependent_response' in sampled_parameters.keys() and sampled_parameters['frequency_dependent_response']: + self.SampleFrequencyDependence() + sample_values.extend([np.mean(self.p_factors), np.mean(self.q_factors)]) + self.use_frequency_dependent_lineshape = True + + + logger.info('Samples are: {}'.format(sample_values)) + #logger.info('Fit parameters: \n{}\nFixed: {}'.format(self.parameter_names, self.fixes)) + # set new values in model + self.ConfigureFit() + + return self.parameter_samples + + def SampleFrequencyDependence(self): + self.p_factors = self.Gaussian_sample(self.p_factors_mean, self.p_factors_width) + self.q_factors = self.Gaussian_sample(self.q_factors_mean, self.q_factors_width) + + # ========================================================================= + # Frequency - Energy conversion + # ========================================================================= + + def Energy(self, f, mixfreq=0.): + """ + converts frequency in Hz to energy in eV + """ + emass = constants.electron_mass/constants.e*constants.c**2 + gamma = (constants.e*self.B)/(2.0*np.pi*constants.electron_mass) * 1./(np.array(f)+mixfreq) + + return (gamma -1.)*emass + + + def Frequency(self, E, Theta=None): + """ + converts energy in eV to frequency in Hz + """ + if Theta==None: + Theta=np.pi/2 + + emass = constants.electron_mass/constants.e*constants.c**2 + gamma = E/(emass)+1. + + return (constants.e*self.B)/(2.0*np.pi*constants.electron_mass) * 1./gamma + + + + # ========================================================================= + # Bin and energy array set&get + # ========================================================================= + + @property + def energies(self): + return self._energies + + @energies.setter + def energies(self, some_energies): + """ + fine grained array. + delete pre-existing efficiencies when resetting energies. + """ + self._energies = some_energies + + self._full_efficiency, self._full_efficiency_errors = [], [] + efficiency = self.Efficiency(self._energies) + self._full_efficiency = efficiency[0] + self._full_efficiency_errors = efficiency[1] + + """super(Tritium, self).__init__(a=np.min(self._energies), + b=np.max(self._energies), + xtol=self.xtol, seed=self.seed)""" + + + @property + def bins(self): + return self._bins#, self._freq_bin_centers + + @bins.setter + def bins(self, some_bins): + """ + energy bins. + delete pre-existing efficiencies when resetting bins. + """ + + # energy bins + self._bins = some_bins + if len(self._energies) > self.N_energy_bins: + self._energies = self._energies[:-1] + if len(self._bins) > self.N_bins: + self._bins = self._bins[:-1] + + self.bin_centers = self._bins[0:-1] +0.5*(self._bins[1]-self._bins[0]) + + # frequency bins + self.freq_bins = self.Frequency(self._bins) + self.freq_bin_centers = self.Frequency(self.bin_centers) + + # bin efficiencies + self._bin_efficiency, self._bin_efficiency_errors = [], [] + self._bin_efficiency, self._bin_efficiency_errors = self.Efficiency(self.freq_bin_centers, freq=True) + + + def ReSetBins(self): + #self.energies = np.arange(self.Energy(self.max_frequency), self.Energy(self.min_frequency), self.denergy) + #self.bins = np.arange(np.min(self.energies), np.max(self.energies), self.dbins) + + #self._bin_efficiency, self._bin_efficiency_error = [], [] + self.energies = np.arange(self.Energy(self.max_frequency), self.Energy(self.max_frequency)+(self.N_energy_bins)*self.denergy, self.denergy) + self.bins = np.arange(np.min(self.energies), np.min(self.energies)+(self.N_bins)*self.dbins, self.dbins) + self._bin_efficiency, self._bin_efficiency_error = [], [] + + if len(self._energies) > self.N_energy_bins: + self._energies = self._energies[:-1] + if len(self._bins) > self.N_bins: + self._bins = self._bins[:-1] + + if self.use_relative_livetime_correction: + self.channel_energy_edges = self.Energy(self.channel_transition_freqs) + #logger.info('Channel energy edges: {}'.format(self.channel_energy_edges)) + + + + # ========================================================================= + # Data generation and histogramming + # ========================================================================= + + def GenerateData(self, params, N): + + x = self.energies[0:-1]+0.5*(self.energies[1]-self.energies[0]) + pdf = np.longdouble(self.TritiumSpectrumBackground(x, *params)) + pdf[pdf<0]=0. + np.random.seed() + + pdf = np.float64(np.longdouble(pdf/np.sum(pdf))) + + self.data = np.random.choice(x, np.random.poisson(N), p=pdf/np.sum(pdf)) + self.freq_data = self.Frequency(self.data) + + return self.data, self.freq_data + + + def GenerateAsimovData(self, params): + + asimov_data = [] + #x = self.bins #np.arange(min(self.energies), max(self.energies), 25) + #xc = x[0:-1]+0.5*(x[1]-x[0]) + #pdf = np.round(self.TritiumSpectrumBackground(xc, *params)) + + self.use_asimov = True + #for i, p in enumerate(pdf): + # asimov_data.extend(list(itertools.repeat(xc[i], int(p)))) + + return np.array(asimov_data), self.Frequency(np.array(asimov_data)) + + + + def ConvertFreqData2EnergyData(self): + """ + Convert frequencies to energyies with set B + """ + self.data = self.Energy(self.freq_data) + return self.data + + + def Histogram(self, weights=None): + """ + histogram data using bins + """ + h, b = np.histogram(self.data, bins=self.bins, weights=weights) + self.hist = h#float2double(h) + return h + + + def ConvertAndHistogram(self, weights=None): + """ + histogram data using bins + """ + self.data = self.ConvertFreqData2EnergyData() + self.hist, b = np.histogram(self.data, bins=self.bins, weights=weights) + #self.hist = float2double(h) + return self.hist + + + + + + + + + # ========================================================================= + # Model functions + # ========================================================================= + + + def gauss_resolution_f(self, energy_array, A, sigma, mu): + f = A*(1/(sigma*np.sqrt(2*np.pi)))*np.exp(-(((energy_array-mu)/sigma)**2.)/2.) + return f + + def derived_two_gaussian_resolution(self, energy_array, sigma_s, mu_1, mu_2, A=1): + sigma_1 = (sigma_s-self.two_gaussian_p0 + self.two_gaussian_fraction * self.two_gaussian_p0)/(self.two_gaussian_fraction + self.two_gaussian_p1 - self.two_gaussian_fraction* self.two_gaussian_p1) + sigma_2 = self.two_gaussian_p0 + self.two_gaussian_p1 * sigma_1 + + lineshape = self.two_gaussian_fraction * self.gauss_resolution_f(energy_array, 1, sigma_1*self.width_scaling, mu_1) + (1 - self.two_gaussian_fraction) * self.gauss_resolution_f(energy_array, 1, sigma_2*self.width_scaling, mu_2) + return lineshape + + + def beta_rates(self, K, Q, m_nu_squared, shape='mainz'):#, index): + spectrum = np.zeros(len(K)) + shape = self.neg_mbeta_squared_model + + Q_minus_K = Q-K + + if m_nu_squared >= 0: + nu_mass_shape_squared = (Q_minus_K)**2 -m_nu_squared + index = np.where((nu_mass_shape_squared>0) & (Q_minus_K>0)) + nu_mass_shape = np.sqrt(nu_mass_shape_squared[index]) + spectrum[index] = (Q_minus_K[index])*nu_mass_shape#*self.ephasespace(K[index], Q) + elif shape=='mainz': + # mainz shape for negative mbeta**2 + k_squared = -m_nu_squared + mu = 0.66*np.sqrt(k_squared) + index = np.where(Q_minus_K+mu>0) + spectrum[index] = (Q_minus_K[index]+mu*np.exp(-1-Q_minus_K[index]/mu))*np.sqrt(Q_minus_K[index]**2+k_squared)#*self.ephasespace(K[index], Q) + elif shape=='lanl': + # lanl shape for negative mbeta**2 + k_squared = -m_nu_squared + index = np.where(Q_minus_K > 0) + spectrum[index] = Q_minus_K[index]**2+k_squared/2 + elif shape=='llnl': + #print('llnl') + k_squared = -m_nu_squared + index = np.where(Q_minus_K+np.sqrt(k_squared)>0) + spectrum[index] = np.abs((Q_minus_K[index])**2+k_squared*Q_minus_K[index]/(2*np.abs(Q_minus_K[index]))) + elif shape=='katrin': + k_squared = -m_nu_squared + index = np.where(Q_minus_K>0) + spectrum[index] = Q_minus_K[index]*np.sqrt((Q_minus_K[index])**2+k_squared) + #print(Q_minus_K) + #print(len(K[self.index])) + #print(len(K)) + #print(np.max(K[index])) + #print(np.max(K)) + + return spectrum + + + def approximate_spectrum(self, E, Q, m_nu_squared=0): + """ + model as in mermithid fake data generator: + https://github.com/project8/mermithid/blob/feature/phase2-analysis/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py + + but the ephasespace is approximate and neutrino parameter is mass squared (some factors neglected) + """ + + if self.use_final_states: + N_states = len(self.final_state_array[0]) + Q_states = Q+self.final_state_array[0]-np.max(self.final_state_array[0]) + + #index = [np.where(((Q_states[i]-E)**2-m_nu_squared > 0) & (Q_states[i]-E > 0)) for i in range(N_states)] + #index = [np.where(E < Q_states[i] -mnu) for i in range(N_states)] + beta_rates_array = [self.beta_rates(E, Q_states[i], m_nu_squared)#, index[i]) + * self.final_state_array[1][i] + for i in range(N_states)] + + spectrum = GF**2.*Vud**2*Mnuc2/(2.*np.pi**3)*np.nansum(beta_rates_array, axis=0)/np.nansum(self.final_state_array[1])*self.ephasespace(E, Q) + + # convolve final state spectrum + # max_energy = self.max_final_state_energy_loss + # dE = self.energies[1]-self.energies[0]#E[1]-E[0] + # n_dE = round(max_energy/dE) + # e_lineshape = np.arange(-n_dE*dE, n_dE*dE, dE) + # spectrum_nomfs = GF**2.*Vud**2*Mnuc2/(2.*np.pi**3)*self.beta_rates(E, Q, m_nu_squared) + # spectrum = convolve(spectrum_nomfs, self.final_states_interp(e_lineshape), mode='same') + + else: + spectrum = GF**2.*Vud**2*Mnuc2/(2.*np.pi**3)*self.beta_rates(E, Q, m_nu_squared)*self.ephasespace(E, Q) + + if self.use_relative_livetime_correction: + # scale spectrum in frequency ranges according to channel livetimes + channel_a_index = np.where((Eself.channel_energy_edges[0][1])) + channel_b_index = np.where((Eself.channel_energy_edges[1][1])) + channel_c_index = np.where((Eself.channel_energy_edges[2][1])) + + spectrum[channel_a_index] = spectrum[channel_a_index]*self.channel_relative_livetimes[0] + spectrum[channel_b_index] = spectrum[channel_b_index]*self.channel_relative_livetimes[1] + spectrum[channel_c_index] = spectrum[channel_c_index]*self.channel_relative_livetimes[2] + + return spectrum + + def ephasespace(self, K, Q): + #G = rad_corr(K, Q) #Radiative correction + #S = screen_corr(K) #Screening factor + #I = exchange_corr(K) #Exchange correction + #R = recoil_corr(K, Q) #Recoil effects + #LC = finite_nuc_corr(K, Q) #Finite nucleus corrections + #X = coul_corr(K, Q) #Recoiling Coulomb field correction + F = fermi_func(K) #Uncorrected Fermi function + return pe(K)*Ee(K)*F#*G*S*I*R*LC*X + + def which_model(self, *pars): + if self.use_relative_livetime_correction: + return self.approximate_spectrum(*pars) + #return self.chopped_approximate_spectrum(*pars) + if self.use_approx_model: + return self.approximate_spectrum(*pars) + else: + return self.effective_TritiumSpectrumShape(*pars) + + def mode_exp_scatter_peak_ratio(self, prob_b, prob_c, j): + ''' + ratio of successive peaks taking reconstruction efficiency into account + ''' + c = -self.spr_factor*prob_b + prob_c + return np.exp(-prob_b*j**c) + + + def simplified_ls(self, K, Kcenter, FWHM, prob_b, prob_c=1): + """ + Simplified lineshape. sum of Gaussians imitating hydrogen only lineshape + """ + + p0, p1, p2, p3 = self.lineshape_p[1], self.lineshape_p[3], self.lineshape_p[5], self.lineshape_p[7] + sig0 = FWHM/float(2*np.sqrt(2*np.log(2))) + shape = np.zeros(len(K))#gaussian(K, [sig0, Kcenter]) + norm = 1. + + for i in range(self.NScatters): + sig = p0[i]+p1[i]*FWHM + mu = -(p2[i]+p3[i]*np.log(FWHM-30)) + + if self.use_fixed_scatter_peak_ratio: + probi = prob_b**(i+1) + else: + probi = self.mode_exp_scatter_peak_ratio(prob_b, prob_c, i+1) + + shape += probi*self.gauss_resolution_f(K, 1, sig, mu+Kcenter) + norm += probi + + return shape, norm + + def f_i(self, K, gamma, mu, sigma): + z = (mu+gamma*sigma**2+K)/(np.sqrt(2)*sigma) + f = np.exp(gamma*(mu+K+gamma*sigma**2/2.))*erfc(z) + return f/np.sum(f) + + + + + def more_accurate_simplified_multi_gas_lineshape(self, K, Kcenter, FWHM, prob_b, prob_c=1, h2_fraction=1): + """ + Still a simplified lineshape but helium is not just a gaussian + """ + if self.plot_lineshape: + logger.info('Using more accurate multi gas scattering. Hydrogen proportion is {}'.format(self.h2_fraction)) + + + p0, p1, p2, p3 = self.lineshape_p[1], self.lineshape_p[3], self.lineshape_p[5], self.lineshape_p[7] + q0, q1, q2, q3, q4, q5, q6, q7 = self.helium_lineshape_p[1], self.helium_lineshape_p[3],\ + self.helium_lineshape_p[5], self.helium_lineshape_p[7],\ + self.helium_lineshape_p[9], self.helium_lineshape_p[11],\ + self.helium_lineshape_p[13], self.helium_lineshape_p[15] + + + + # sig0 = FWHM/float(2*np.sqrt(2*np.log(2))) + #shape0 = self.gauss_resolution_f(K, 1, sig0, Kcenter) + #shape0 *= 1/np.sum(shape0) + shape0 = np.zeros(len(K)) + norm = 1. + norm_h = 1. + norm_he = 1. + + hydrogen_scattering = np.zeros(len(K)) + helium_scattering = np.zeros(len(K)) + + if FWHM < 30: + FWHM = 30 + + #plt.figure(figsize=(10,10)) + + for i in range(self.NScatters): + + # hydrogen scattering + sig = p0[i]+p1[i]*FWHM + mu = -(p2[i]+p3[i]*np.log(FWHM-30)) + + if self.use_fixed_scatter_peak_ratio: + probi = prob_b**(i+1) + else: + probi = self.mode_exp_scatter_peak_ratio(prob_b, prob_c, i+1) + + h_scatter_i = self.gauss_resolution_f(K, 1, sig, mu+Kcenter) + hydrogen_scattering += probi*h_scatter_i/np.sum(h_scatter_i) + norm += probi + #plt.plot(K, h_scatter_i, color='blue', label='hydrogen') + + # helium scattering + mu_he = (q0[i]+q1[i]*FWHM+q2[i]*FWHM**2) + sig_he = q3[i]+q4[i]*FWHM + gamma_he = q5[i]+q6[i]*FWHM+q7[i]*FWHM**2 + he_scatter_i = self.f_i(K, gamma_he, mu_he, sig_he) + helium_scattering += probi * he_scatter_i + + #plt.plot(K, (he_scatter_i/(np.sum(he_scatter_i)*(K[1]-K[0]))), color='cyan') + + #plt.plot(K, (shape0 + hydrogen_scattering)/np.max(shape0 + hydrogen_scattering), color='blue', label='hydrogen') + #plt.plot(K, (shape0 + helium_scattering)/np.max(shape0 + helium_scattering), color='red', label='helium') + + # full lineshape + #norm_h = np.sum(shape0 + hydrogen_scattering) + #norm_he = np.sum(shape0 + helium_scattering) + + lineshape = (h2_fraction*(shape0 + hydrogen_scattering)/norm_h + + (1-h2_fraction)*(shape0 + helium_scattering)/norm_he) + + #plt.plot(K, lineshape/np.max(lineshape), color='darkgreen', label='full: {} hydrogen'.format(self.hydrogen_proportion)) + #plt.xlim(-200, 200) + #plt.legend() + + return lineshape, norm + + def simplified_multi_gas_lineshape(self, K, Kcenter, FWHM, prob_b, prob_c=1, h2_fraction=1): + """ + This uses Gaussians of different mu and sigma for different gases + """ + + if self.plot_lineshape: + logger.info('Using simplified lineshape. Hydrogen proportion is {}'.format(h2_fraction)) + + p0, p1, p2, p3 = self.lineshape_p[1], self.lineshape_p[3], self.lineshape_p[5], self.lineshape_p[7] + q0, q1, q2, q3 = self.helium_lineshape_p[1], self.helium_lineshape_p[3], self.helium_lineshape_p[5], self.helium_lineshape_p[7] + + + + sig0 = FWHM/float(2*np.sqrt(2*np.log(2))) + #if sig0 < 6: + # logger.warning('Scatter resolution {} < 6 eV. Setting to 6 eV'.format(sig0)) + # sig0 = 6 + #shape = self.gauss_resolution_f(K, 1, sig0, Kcenter) + #shape = np.zeros(len(K)) + norm = 1. + + hydrogen_scattering = np.zeros(len(K)) + helium_scattering = np.zeros(len(K)) + + #plt.figure(figsize=(10,10)) + + #scatter_peaks = np.array([[self.gauss_resolution_f(K, 1, p2[i]+p3[i]*sig0, -p0[i]+p1[i]*sig0+Kcenter)*self.mode_exp_scatter_peak_ratio(prob_b, prob_c, i+1), + # self.gauss_resolution_f(K, 1, q2[i]+q3[i]*sig0, -q0[i]+q1[i]*sig0+Kcenter)*self.mode_exp_scatter_peak_ratio(prob_b, prob_c, i+1)] for i in range(self.NScatters)]) + + + for i in range(self.NScatters): + + # hydrogen scattering + mu = -p0[i]+p1[i]*sig0 + sig = p2[i]+p3[i]*sig0 + + if self.use_fixed_scatter_peak_ratio: + probi = prob_b**(i+1) + else: + probi = self.mode_exp_scatter_peak_ratio(prob_b, prob_c, i+1) + + h_scatter_i = probi*self.gauss_resolution_f(K, 1, sig, mu+Kcenter) + hydrogen_scattering += h_scatter_i + norm += probi + #plt.plot(K, h_scatter_i, color='blue', label='hydrogen') + + # helium scattering + mu_he = -q0[i]+q1[i]*sig0 + sig_he = q2[i]+q3[i]*sig0 + he_scatter_i = probi*self.gauss_resolution_f(K, 1, sig_he, mu_he+Kcenter) + helium_scattering += he_scatter_i + + #plt.plot(K, he_scatter_i, color='red', label='helium') + + #plt.plot(K, (shape + hydrogen_scattering)/np.max(shape + hydrogen_scattering), color='blue', label='hydrogen') + #plt.plot(K, (shape + helium_scattering)/np.max(shape + helium_scattering), color='red', label='helium') + # full lineshape + #lineshape = self.hydrogen_proportion*hydrogen_scattering + (1-self.hydrogen_proportion)*helium_scattering + lineshape = h2_fraction*hydrogen_scattering + (1-h2_fraction)*helium_scattering + + #plt.plot(K, lineshape/np.max(lineshape), color='black', label='full') + #plt.xlim(-200, 200) + #plt.legend() + + return lineshape, norm + + """def complex_lineshape(self, K, Kcenter, FWHM, prob_b, prob_c=1): + lineshape_rates = self.complexLineShape.spectrum_func_1(K/1000., FWHM, 0, 1, prob_b) + plt.plot(K, lineshape_rates/np.max(lineshape_rates), label='complex lineshape', color='purple', linestyle='--') + return lineshape_rates""" + + def scatter_peaks(self, K, Kcenter, FWHM): + + if self.plot_lineshape: + logger.info('Using simplified scatter peaks.') + + p0, p1, p2, p3 = self.lineshape_p[1], self.lineshape_p[3], self.lineshape_p[5], self.lineshape_p[7] + q0, q1, q2, q3 = self.helium_lineshape_p[1], self.helium_lineshape_p[3], self.helium_lineshape_p[5], self.helium_lineshape_p[7] + + + + sig0 = FWHM/float(2*np.sqrt(2*np.log(2))) + #if sig0 < 6: + # logger.warning('Scatter resolution {} < 6 eV. Setting to 6 eV'.format(sig0)) + # sig0 = 6 + #shape = self.gauss_resolution_f(K, 1, sig0, Kcenter) + #shape = np.zeros(len(K)) + norm = 1. + #hydrogen_scattering = np.array([self.gauss_resolution_f(K, 1, p2[i]+p3[i]*sig0, (-p0[i]+p1[i]*sig0)+Kcenter) for i in range(self.NScatters)]) + #helium_scattering = np.array([self.gauss_resolution_f(K, 1, q2[i]+q3[i]*sig0, (-q0[i]+q1[i]*sig0)+Kcenter) for i in range(self.NScatters)]) + + hydrogen_scattering = np.zeros((self.NScatters, len(K))) + helium_scattering = np.zeros((self.NScatters, len(K))) + + + for i in range(self.NScatters): + + # hydrogen scattering + mu = -p0[i]+p1[i]*sig0 + sig = p2[i]+p3[i]*sig0 + hydrogen_scattering[i] = self.gauss_resolution_f(K, 1, sig, mu+Kcenter) + + # helium scattering + mu_he = -q0[i]+q1[i]*sig0 + sig_he = q2[i]+q3[i]*sig0 + helium_scattering[i] = self.gauss_resolution_f(K, 1, sig_he, mu_he+Kcenter) + + return hydrogen_scattering, helium_scattering + + def running_mean(self, x, N): + N_round = round(N) + cumsum = np.cumsum(x) + return (cumsum[int(N_round)::int(N_round)] - cumsum[:-int(N_round):int(N_round)]) / float(N_round) + + + + def TritiumSpectrum(self, E=[], *args, error=False):#endpoint=18.6e3, m_nu=0., prob_b=None, prob_c=None, res=None, sig1=None, sig2=None, tilt=0., error=False): + E = np.array(E) + + + # tritium args + tritium_args = np.array(args)[self.tritium_model_indices] + if 'B' in self.model_parameter_names: + if 'B' in self.parameter_samples.keys() and not self.parameter_samples['B']: + self.B = args[self.B_index] + #self.ReSetBins() + self.ConvertAndHistogram() + + + if len(E)==0: + E = self._bin_centers + + """############### E is float ############### + if isinstance(E, float): + if E+m_nu > endpoint: + K_spec = 0. + else: + + efficiency = 1. + if self.is_distorted == True: + efficiency = self.Efficiency(E)[0] + K_spec = self.which_model(E, endpoint, m_nu)*efficiency/self.norm""" + + + ################## E is list or array ################# + + + # smear spectrum + if self.is_smeared or self.is_scattered: + + # resolution params + if 'resolution' not in self.model_parameter_names or 'resolution' in self.parameter_samples.keys():#self.fixed_parameters[self.res_index]: + res = self.res + #logger.info('Using self.res') + else: + res = args[self.res_index] + + if self.resolution_model != 'gaussian': + if self.derived_two_gaussian_model: + if 'two_gaussian_mean_1' not in self.model_parameter_names or 'two_gaussian_mean_1' in self.parameter_samples.keys(): + two_gaussian_mu_1 = self.two_gaussian_mu_1 + two_gaussian_mu_2 = self.two_gaussian_mu_2 + else: + two_gaussian_mu_1 = args[self.two_gaussian_mu_1_index] + two_gaussian_mu_2 = args[self.two_gaussian_mu_2_index] + + else: + + if 'two_gaussian_sigma_1' not in self.model_parameter_names or 'two_gaussian_sigma_1' in self.parameter_samples.keys(): + sig1 = self.two_gaussian_sigma_1 + sig2 = self.two_gaussian_sigma_2 + #logger.info('Using self.two_gaussian_sigma_1') + else: + sig1 = args[self.two_gaussian_sigma_1_index] + sig2 = args[self.two_gaussian_sigma_2_index] + + + max_energy = self.max_energy + dE = self.energies[1]-self.energies[0]#E[1]-E[0] + n_dE = round(max_energy/dE) + #e_add = np.arange(np.min(self.energies)-round(max_energy/dE)*dE, np.min(self.energies), dE) + e_lineshape = np.arange(-n_dE*dE, n_dE*dE, dE) + + + e_spec = np.arange(min(self.energies)-max_energy, max(self.energies)+max_energy, dE) + #e_spec = self.energies + #np.r_[e_add, self._energies] + + # energy resolution + if self.resolution_model != 'two_gaussian': + lineshape = self.gauss_resolution_f(e_lineshape, 1, res*self.width_scaling, 0) + elif self.derived_two_gaussian_model: + lineshape = self.derived_two_gaussian_resolution(e_lineshape, res, two_gaussian_mu_1, two_gaussian_mu_2) + else: + lineshape = self.two_gaussian_wide_fraction * self.gauss_resolution_f(e_lineshape, 1, sig1*self.width_scaling, self.two_gaussian_mu_1) + (1 - self.two_gaussian_wide_fraction) * self.gauss_resolution_f(e_lineshape, 1, sig2*self.width_scaling, self.two_gaussian_mu_2) + + # spectrum shape + spec = self.which_model(e_spec, *tritium_args)#endpoint, m_nu) + #spec[np.where(e_spec>args[self.endpoint_index]-np.abs(m_nu)**0.5)]=0. + + if not self.is_scattered: + # convolve with gauss with spectrum + K_convolved = convolve(spec, lineshape, mode='same') + below_Kmin = np.where(e_spec < min(self.energies)) + #np.put(K_convolved, below_Kmin, np.zeros(len(below_Kmin))) + K_convolved = np.interp(self.energies, e_spec, K_convolved) + #K_convolved = K_convolved[e_spec>=np.min(self.energies)] + + + + if self.is_scattered: + # scatter params + if 'scatter_peak_ratio_p' not in self.model_parameter_names: + prob_b = self.scatter_peak_ratio_p + prob_c = self.scatter_peak_ratio_q + else: + prob_b = args[self.scatter_peak_ratio_p_index] + prob_c = args[self.scatter_peak_ratio_q_index] + + if self.fixed_parameters[self.scatter_peak_ratio_p_index]: + prob_b = self.scatter_peak_ratio_p + if self.fixed_parameters[self.scatter_peak_ratio_q_index]: + prob_c = self.scatter_peak_ratio_q + + if 'h2_fraction' in self.model_parameter_names: + h2_fraction = args[self.h2_fraction_index] + else: + h2_fraction = self.h2_fraction + + # simplified lineshape + FWHM = 2.*np.sqrt(2.*np.log(2.))*res *self.width_scaling + + # add tail + if not self.use_helium_scattering: + tail, norm = self.simplified_ls(e_lineshape, 0, FWHM, prob_b, prob_c) + lineshape += tail + lineshape = lineshape/norm + K_convolved = convolve(spec, lineshape, mode='same') + if self.plot_lineshape: + logger.info('Using simplified lineshape model') + + elif self.use_frequency_dependent_lineshape: + Kbounds = [e_spec[0]] + list(self.scatter_factor_bounds) + [e_spec[-1]] + + peaks = [self.scatter_peaks(e_lineshape, 0, FWHM*self.width_factors[i]) for i in range(len(self.p_factors))] + #hydrogen_peaks, helium_peaks = self.scatter_peaks(e_lineshape, 0, FWHM) + scatter_peak_ratios = [self.mode_exp_scatter_peak_ratio(prob_b*self.p_factors[j], prob_c*self.q_factors[j], np.arange(self.NScatters)+1) for j in range(len(self.p_factors))] + norms = np.sum(scatter_peak_ratios, axis=1) + + lineshape = np.array([self.derived_two_gaussian_resolution(e_lineshape, res*self.width_factors[i], two_gaussian_mu_1, two_gaussian_mu_2) for i in range(len(self.p_factors))]) + hydrogen_tail = np.array([np.sum(np.multiply(peaks[i][0], scatter_peak_ratios[i][:, None]), axis=0)*h2_fraction for i in range(len(self.p_factors))]) + helium_tail = np.array([np.sum(peaks[i][1]*scatter_peak_ratios[i][:, None], axis=0)*(1-h2_fraction) for i in range(len(self.p_factors))]) + detector_response = (hydrogen_tail+helium_tail+lineshape)/norms[:,None] + + + + #tails_and_norms = [self.multi_gas_lineshape(e_lineshape, 0, FWHM, prob_b*self.p_factors[i], prob_c*self.q_factors[i], h2_fraction) for i in range(len(self.p_factors))] + K_convolved_segments = [convolve(spec, detector_response[i], mode='same')[np.logical_and(Kbounds[i]<=e_spec, e_spec<=Kbounds[i+1])] for i in range(len(self.p_factors))] + + K_convolved = np.concatenate(K_convolved_segments, axis=None) + + if self.plot_lineshape: + logger.info('Using two gas simplified lineshape model with frequency dependent p & q') + lineshape = detector_response[0] + else: + tail, norm = self.multi_gas_lineshape(e_lineshape, 0, FWHM, prob_b, prob_c, h2_fraction) + lineshape += tail + lineshape = lineshape/norm + K_convolved = convolve(spec, lineshape, mode='same') + if self.plot_lineshape: + logger.info('Using two gas simplified lineshape model') + + try: + K_convolved = np.interp(self.energies, e_spec, K_convolved) + except Exception as e: + print(np.shape(Kbounds)) + print(np.shape(self.p_factors)) + print(np.shape(e_spec), np.shape(K_convolved), np.shape(spec),np.shape(detector_response)) + raise e + + + + #logger.info('Plotting lineshape now: {}'.format(self.plot_lineshape)) + if self.plot_lineshape: + logger.info('Plotting confirmed') + #ax.plot(e_gauss, full_lineshape, label='Full lineshape', color='grey') + # print(FWHM, ratio) + # ax.set_xlabel('Energy [eV]') + # ax.set_ylabel('Amplitude') + # ax.legend(loc='best') + # plt.xlim(-500, 250) + # plt.tight_layout() + # plt.savefig(os.path.join(self.savepath, 'scattering_model.pdf'), dpi=200, transparent=True) + + plt.figure(figsize=(7,5)) + #plt.plot(e_lineshape, self.simplified_ls(e_lineshape, 0, FWHM, ratio), color='red', label='FDG') + if self.resolution_model != 'two_gaussian': + resolution = self.gauss_resolution_f(e_lineshape, 1, res, 0) + logger.info('Using Gaussian resolution model') + elif self.derived_two_gaussian_model: + resolution = self.derived_two_gaussian_resolution(e_lineshape, res, two_gaussian_mu_1, two_gaussian_mu_2) + logger.info('Using derived two Gaussian resolution model') + else: + resolution = self.two_gaussian_fraction * self.gauss_resolution_f(e_lineshape, 1, sig1*self.width_scaling, self.two_gaussian_mu_1) + (1 - self.two_gaussian_fraction) * self.gauss_resolution_f(e_lineshape, 1, sig2*self.width_scaling, self.two_gaussian_mu_2) + logger.info('Using two Gaussian resolution model') + + + plt.plot(e_lineshape, resolution/np.max(resolution), label = 'Resolution', color='orange') + plt.plot(e_lineshape, lineshape/np.max(lineshape), label = 'Full lineshape', color='Darkblue') + + + FWHM = 2.*np.sqrt(2.*np.log(2.))*res*self.width_scaling + + #logger.info('Plotting lineshape for FWHM {} and hydrogen proportion {}.'.format(FWHM, self.hydrogen_proportion)) + #simple_ls, simple_norm = self.simplified_ls(e_lineshape, 0, FWHM, prob_b, prob_c) + #simple_ls = (self.gauss_resolution_f(e_lineshape, 1, res, 0)+simple_ls)/simple_norm + #plt.plot(e_lineshape, simple_ls/np.nanmax(simple_ls), label='Hydrogen only lineshape', color='red') + plt.xlabel('Energy [eV]') + plt.ylabel('Amplitude') + plt.grid() + plt.xlim(-500, 250) + plt.legend(loc='best') + plt.tight_layout() + plt.savefig(os.path.join(self.savepath, 'lineshape.pdf'), dpi=200, transparent=True) + + + + else: + # base shape + spec = self.which_model(e_spec, *tritium_args) + K_convolved = spec + + # Fake integration + if self.integrate_bins: + + dE = self.denergy + dE_bins = E[1]-E[0] + N = np.round(dE_bins/dE,4) + + if not (np.abs(N%1) < 1e-6 or np.abs(N%1 - 1)<1e-6): + logger.error('N', N) + logger.error('modulo', N%1, N%1-1) + raise ValueError('bin sizes have to divide') + + + if N > 1: + #print('Integrate spectrum', len(K_convolved)) + if E[0] -0.5*dE_bins >= self._energies[1]: + K_convolved_cut = K_convolved[self.energies>=min(E[0] -0.5*dE_bins)] + K_convolved = self.running_mean(K_convolved_cut, N) + logger.warning('Cutting spectrum below Kmin:{} > {}'.format(E[0]-0.5*dE_bins, self._energies[0])) + else: + K_convolved = self.running_mean(K_convolved, N) + + else: + # till here K_convolved was defiend on self._energies + # now we need it on E + K_convolved = np.interp(E, self.energies, K_convolved)*(len(E)*1./len(self._energies)) + + + else: + #logger.warning('WARNING: tritium spectrum is not integrated over bin widths') + K_convolved = np.interp(E, self._energies, K_convolved)*(len(E)*1./len(self._energies)) + + + + if self.is_distorted == True: + #if self.fit_efficiency_tilt: + # self.tilt = tilt + efficiency, efficiency_errors = self.Efficiency(E) + else: + # multiply efficiency + efficiency = np.ones(len(E)) + efficiency_errors = [np.zeros(len(E)), np.zeros(len(E))] + + K_eff=K_convolved*efficiency + # finally + K = K_eff/np.nansum(K_eff)*np.nansum(K_convolved) + + # if error from efficiency on spectrum shape should be returned + if error: + K_error=np.zeros((2, len(E))) + K_error = K_convolved*(efficiency_errors)/np.nansum(K_eff)*np.nansum(K_convolved) + + return K, K_error + else: + return K + + + + def TritiumSpectrumBackground(self, E=[], *args): + + if len(E)==0: + E = self.bin_centers + + if self.pass_efficiency_error: + K, K_error = self.TritiumSpectrum(E, *args, error=True)#endpoint, m_nu, prob_b, prob_c, res, sig1, sig2, tilt) + K_norm = np.sum(self.TritiumSpectrum(self._energies, *args))#endpoint, m_nu, prob_b, prob_c, res, sig1, sig2, tilt)*(self._energies[1]-self._energies[0])) + else: + + K = self.TritiumSpectrum(E, *args)#endpoint, m_nu, prob_b, prob_c, res, sig1, sig2, tilt) + K_norm = np.sum(self.TritiumSpectrum(self._energies, *args))#endpoint, m_nu, prob_b, prob_c, res, sig1, sig2, tilt)*(self._energies[1]-self._energies[0])) + + + + + if isinstance(E, float): + print('E is float, only returning tritium amplitude') + return K*(E[1]-E[0])/K_norm + else: + + b = args[self.background_index]/(max(self._energies)-min(self._energies)) + a = args[self.amplitude_index]#-b + + B = np.ones(np.shape(E)) + B = B*b*(E[1]-E[0])#/(self._energies[1]-self._energies[0]) + #B= B/np.sum(B)*background + + + K = (K/K_norm*(E[1]-E[0]))*a + #K[E>endpoint-np.abs(m_nu**0.5)+de] = np.zeros(len(K[E>endpoint-np.abs(m_nu**2)+de])) + #K= K/np.sum(K)*a + #K = K+B + + + + if self.pass_efficiency_error: + K_error = K_error*(E[1]-E[0])/K_norm*a + #K_error = K_error/np.sum(K)*a + return K+B, K_error + else: return K+B + + + def normalized_TritiumSpectrumBackground(self, E=[], *args):#endpoint=18.6e3, background=0, m_nu=0., amplitude=1., prob_b=None, prob_c=None, res=None, sig1=None, sig2=None, tilt=0., error=False): + + if self.pass_efficiency_error: + t, t_error = self.TritiumSpectrumBackground(E, *args)#endpoint, background, m_nu, amplitude, prob_b, prob_c, res, sig1, sig2, tilt) + t_norm = np.sum(t) + return t/t_norm, t_error/t_norm + else: + t = self.TritiumSpectrumBackground(E, *args)#endpoint, background, m_nu, amplitude, prob_b, prob_c, res, sig1, sig2, tilt) + t_norm = np.sum(t) + return t/t_norm + + + + ################# SNR - Efficiency functions ##################### + + + def Efficiency(self, E, freq=False, pseudo=False): + """ + get efficiencies for energy or frequency bins + + freq: if true, E is assumed to be frequencies instead of energies + """ + + if not freq: + f = self.Frequency(E) + else: + f = deepcopy(E) + E = self.Energy(f) + + + # if E is float + if isinstance(f, float): + #print('Calculating single efficiency value') + f = np.array([f]) + if not phase_4: + df = 1e6 + else: + df = 1 + if pseudo == False: + print('best efficiency') + efficiency, errors = integrated_efficiency(f, self.snr_efficiency_dict, df) + return efficiency, errors + else: + print('pseudo efficiency') + efficiency, errors = pseudo_integrated_efficiency(f, self.snr_efficiency_dict, df, alpha=self.error_scaling) + return efficiency, errors + + + # if E is list or array + else: + + # if efficiency has already been calculated for this array, skip recalculation to save time + if len(f) == len(self._bin_efficiency): + efficiency, errors = self._bin_efficiency, self._bin_efficiency_errors + elif len(f) == len(self._full_efficiency): + efficiency, errors = self._full_efficiency, self._full_efficiency_errors + else: + #logger.info('Calculating efficiency with df = {}...'.format(f[1]-f[0])) + + # toy model efficiency + if self.use_toy_model_efficiency: + efficiency = self.power_eff_interp(f) + errors = self.power_eff_error_interp(f) + + # fss efficiency + else: + efficiency, errors = integrated_efficiency(f, self.snr_efficiency_dict) + + + if self.tilted_efficiency:# and (pseudo or self.fit_efficiency_tilt): + + #efficiency, errors = det_eff.integrated_efficiency(f, self.snr_efficiency_dict) + slope = self.tilt/(1e3) + + logger.info('Tilted efficiencies: {}'.format(self.tilt)) + efficiency *= (1.+slope*(E-17.83e3)) + errors *= (1.+slope*(E-17.83e3)) + + # if we are doing pseudo experiments + if pseudo:# and len(f) < len(self.energies):# and len(f) != len(self._bin_efficiency): + + #logger.info('uncorrelated pseudo efficiencies: {}'.format(self.error_scaling)) + + pseudo_efficiency = np.random.randn(len(f))*np.mean([errors[0], errors[1]], axis=0)*self.error_scaling + #pseudo_efficiency[pseudo_efficiency<0]*=errors[0][pseudo_efficiency<0]*self.error_scaling + #pseudo_efficiency[pseudo_efficiency>=0]*=errors[1][pseudo_efficiency>=0]*self.error_scaling + pseudo_efficiency += efficiency + pseudo_efficiency*=np.sum(efficiency)/np.nansum(pseudo_efficiency) + efficiency = pseudo_efficiency + + if np.min(efficiency) < 0: + index = np.where(efficiency < 0)#E>=np.min(E[efficiency<0])) + efficiency[index] = 0. + + + return efficiency, errors + + + + + diff --git a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py index 7ba04d48..3bfe7c40 100644 --- a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py +++ b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py @@ -1,7 +1,8 @@ ''' Generate binned or pseudo unbinned data -Author: T. Weiss, C. Claessens -Date:4/6/2020 +Author: T. Weiss, C. Claessens, X. Huyan +Date: 4/6/2020 +Updated: 10/19/2021 ''' from __future__ import absolute_import @@ -16,7 +17,8 @@ from morpho.utilities import morphologging, reader from morpho.processors import BaseProcessor from mermithid.misc.FakeTritiumDataFunctions import * -from mermithid.processors.misc.KrComplexLineShape import KrComplexLineShape +from mermithid.processors.misc.MultiGasComplexLineShape import MultiGasComplexLineShape +from mermithid.misc import Constants, ComplexLineShapeUtilities, ConversionFunctions logger = morphologging.getLogger(__name__) @@ -36,25 +38,51 @@ def InternalConfigure(self, params): Configurable parameters are: - Q [eV]: endpoint energy - neutrino_mass [eV]: true neutrino mass - - minf [Hz]: low frequency cut-off (high cutoff is determined from efficiency dict) – Kmin [eV]: low energy cut-off – Kmax [eV]: high energy cut-off + - minf [Hz]: low frequency cut-off + - maxf [Hz]: high frequency cutoff (optional; if not provided, max frequency is determined from efficiency dict) - n_steps: number of energy bins that data will be drawn from - B_field: used for energy-frequency conversion - sig_trans [eV]: width of thermal broadening - other_sig [eV]: width of other broadening - - runtime [s]: used to calculate number of background events + - channel_runtimes [s]: live time for each channel + - channel_bounds [Hz]: inner bounds between channels (one less than the number of channels) - S: number of signal events - - A_b [1/eV/s]: background rate - poisson_stats (boolean): if True number of total events is random - err_from_B [eV]: energy uncertainty originating from B uncertainty - - survival_prob: lineshape parameter - ratio of n+t/nth peak - - scattering_sigma [eV]: lineshape parameter - 0-th peak gaussian broadening standard deviation + – gases: list of strings naming gases to be included in complex lineshape model. Options: 'H2', 'He', 'Kr', 'Ar', 'CO' - NScatters: lineshape parameter - number of scatters included in lineshape - - scatter_proportion: fraction of hydrogen in complex lineshape + - trap_weights: distionary of two lists, labeled 'weights' and 'errors', which respectively include the fractions of counts from each trap and the uncertainties on those fractions + - scatter_peak_ratio_p: "p" in reconstrudction efficiency curve model: e^(-p*i^(-factor*p+q)), where i is the scatter order + - scatter_peak_ratio_q: "q" in the same reconstruction efficiency model + - scatter_peak_ratio_factor: "factor" in the same reconstruction efficiency model + – scatter_proportion: list of proportion of scatters due to each gas in self.gases (in the same order), in complex lineshape + - survival_prob: lineshape parameter - probability of electron staying in the trap between two inelastics scatters (it could escape due to elastics scatters or the inelastics scatters, themselves) + – use_radiation_loss: if True, radiation loss will be included in the complex lineshape; should be set to True except for testing purposes + – resolution_function: string determinign type of resolution function; options are 'simulated_resolution', 'gaussian_resolution', 'gaussian_lorentzian_composite_resolution'. + – ratio_gamma_to_sigma: parameter in gaussian_lorentzian_composite_resolution; see the MultiGasComplexLineShape processor + – gaussian_proportion: also a parameter in gaussian_lorentzian_composite_resolution + – A_array: parameter for Gaussian resolution with variable width; see the MultiGasComplexLineShape processor + - sigma_array: also a parameter for Gaussian resolution with variable width + – fit_recon_eff: if True, determine recon eff parameters by fitting to FTG data, instead of by using parameters given in self.recon_eff_parameters + – use_combined_four_trap_inst_reso: for simulated resolution, combine resolution distributions provided for four different traps + - sample_ins_resolution_errors: if True, and if using a simulated instrumental resolution, the count numbers in that distribution will be sampled from uncertainties + - scattering_sigma [eV]: lineshape parameter - 0-th peak gaussian broadening standard deviation + - min_energy [eV]: minimum of lineshape energy window for convolution with beta spectrum. Same magnitude is used for the max energy of the window. + - scale_factor: width scaling for a simulated instrumental resolution + - ins_res_width_bounds: Bounds (eV) of regions within which the resolution width is approximately constant (and therefore can be parameterized by a single scale_factor). ins_res_width_bounds does not include the outermost bounds - i.e., the Kmin and Kmax. If ins_res_width_bounds is None, then a common scale factor is used for the whole ROI. + - ins_res_width_factors: Factors that are multiplied by scale_factor in each of the regions defined by ins_res_width_bounds. Note that len(ins_res_width_factors) == len(ins_res_width_bounds) + 1. + + - efficiency_path: path to efficiency vs. frequency (and uncertainties) - simplified_scattering_path: path to simplified lineshape parameters + – path_to_osc_strengths_files: path to oscillator strength files containing energy loss distributions for the gases in self.gases + – path_to_scatter_spectra_file: path to scatter spectra file, which is generated from the osc strenth files and accounts for cross scattering + – rad_loss_path: path to file containing data describing radiation loss - path_to_detailed_scatter_spectra_dir: path to oscillator and or scatter_spectra_file - - efficiency_path: path to efficiency vs. frequency (and uncertainties) + – path_to_ins_resolution_data_txt: path to file containing simulated instrumental resolution data, already combined among the four traps + - path_to_four_trap_ins_resolution_data_txt: path to files containing simulated instrumental resolution data for each of the four individual traps + - final_states_file: path to file containing molecular final state binding energies and corresponding probabilities - use_lineshape (boolean): determines whether tritium spectrum is smeared by lineshape. If False, it will only be smeared with a Gaussian - detailed_or_simplified_lineshape: If use lineshape, this string determines which lineshape model is used. @@ -85,27 +113,53 @@ def InternalConfigure(self, params): self.broadening = np.sqrt(self.sig_trans**2+self.other_sig**2) #Total energy broadening (eV) # Phase II Spectrum parameters - self.runtime = reader.read_param(params, 'runtime', 6.57e6) #In seconds. Default time is ~2.5 months. + self.channel_runtimes = reader.read_param(params, 'channel_runtimes', [7185228., 7129663., 7160533.]) + self.channel_bounds = reader.read_param(params, 'channel_bounds', [1.38623121e9+24.5e9, 1.44560621e9+24.5e9]) self.S = reader.read_param(params, 'S', 3300) self.B_1kev = reader.read_param(params, 'B_1keV', 0.1) #Background rate per keV for full runtime - self.A_b = reader.read_param(params, 'A_b', self.B_1kev/float(self.runtime)/1000.) #Flat background activity: events/s/eV - self.B =self.A_b*self.runtime*(self.Kmax-self.Kmin) #Background poisson rate + #self.A_b = reader.read_param(params, 'A_b', self.B_1kev/float(self.runtime)/1000.) #Flat background activity: events/s/eV #No longer in use + #self.B =self.A_b*self.runtime*(self.Kmax-self.Kmin) #Background poisson rate #No longer in use self.poisson_stats = reader.read_param(params, 'poisson_stats', True) self.err_from_B = reader.read_param(params, 'err_from_B', 0.) #In eV, kinetic energy error from f_c --> K conversion #Scattering model parameters - self.survival_prob = reader.read_param(params, 'survival_prob', 0.77) - self.scattering_sigma = reader.read_param(params, 'scattering_sigma', 18.6) + self.gases = reader.read_param(params, 'gases', ['H2', 'He', 'CO']) self.NScatters = reader.read_param(params, 'NScatters', 20) - self.scatter_proportion = reader.read_param(params, 'scatter_proportion', 1.0) + self.trap_weights = reader.read_param(params, 'trap_weights', {'weights':[0.076, 0.341, 0.381, 0.203], 'errors':[0.003, 0.013, 0.014, 0.02]}) + #self.recon_eff_params = reader.read_param(params, 'recon_eff_params', [0.005569990343215976, 0.351, 0.546]) + self.scatter_peak_ratio_p = reader.read_param(params, 'scatter_peak_ratio_p', 1.) + self.scatter_peak_ratio_q = reader.read_param(params, 'scatter_peak_ratio_q', 0.6) + self.scatter_peak_ratio_factor = reader.read_param(params, 'scatter_peak_ratio_factor', 0.5) + self.scatter_proportion = reader.read_param(params, 'scatter_proportion', []) + self.survival_prob = reader.read_param(params, 'survival_prob', 1.) + self.use_radiation_loss = reader.read_param(params, 'use_radiation_loss', True) + self.resolution_function = reader.read_param(params, 'resolution_function', '') + self.ratio_gamma_to_sigma = reader.read_param(params, 'ratio_gamma_to_sigma', 0.8) + self.gaussian_proportion = reader.read_param(params, 'gaussian_proportion', 0.8) + self.A_array = reader.read_param(params, 'A_array', [0.076, 0.341, 0.381, 0.203]) + self.sigma_array = reader.read_param(params, 'sigma_array', [5.01, 13.33, 15.40, 11.85]) + self.fit_recon_eff = reader.read_param(params, 'fit_recon_eff', False) + self.use_combined_four_trap_inst_reso = reader.read_param(params, 'use_combined_four_trap_inst_reso', True) + self.sample_ins_resolution_errors = reader.read_param(params, 'sample_ins_res_errors', True) + self.scattering_sigma = reader.read_param(params, 'scattering_sigma', 18.6) self.min_energy = reader.read_param(params,'min_lineshape_energy', -1000) + self.scale_factor = reader.read_param(params, 'scale_factor', 1.0) + self.ins_res_width_bounds = reader.read_param(params, 'ins_res_width_bounds', None) #Default values here need to be corrected + self.ins_res_width_factors = reader.read_param(params, 'ins_res_width_factors', [1.]) + self.p_factors = reader.read_param(params, 'p_factors', [1.]) + self.q_factors = reader.read_param(params, 'q_factors', [1.]) #paths - self.simplified_scattering_path = reader.read_param(params, 'simplified_scattering_path', '/host/input_data/simplified_scattering_params.txt') - self.detailed_scatter_spectra_path = reader.read_param(params, 'path_to_detailed_scatter_spectra_dir', '.') self.efficiency_path = reader.read_param(params, 'efficiency_path', '') + self.simplified_scattering_path = reader.read_param(params, 'simplified_scattering_path', '/host/input_data/simplified_scattering_params.txt') + self.path_to_osc_strengths_files = reader.read_param(params, 'path_to_osc_strengths_files', '/host/') + self.path_to_scatter_spectra_file = reader.read_param(params, 'path_to_scatter_spectra_file', '/host/') + self.rad_loss_path = reader.read_param(params, 'rad_loss_path', '') + self.path_to_ins_resolution_data_txt = reader.read_param(params, 'path_to_ins_resolution_data_txt', '/host/ins_resolution_all.txt') + self.path_to_four_trap_ins_resolution_data_txt = reader.read_param(params, 'path_to_four_trap_ins_resolution_data_txt', ['/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap1.txt', '/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap2.txt', '/host/T2-1.56e-4/analysis_input/complex-lineshape-inputs/res_cf15.5_trap3.txt', '/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap4.txt']) self.final_states_file = reader.read_param(params, 'final_states_file', '') + self.shake_spectrum_parameters_json_path = reader.read_param(params, 'shake_spectrum_parameters_json_path', 'shake_spectrum_parameters.json') #options self.use_lineshape = reader.read_param(params, 'use_lineshape', True) @@ -114,6 +168,7 @@ def InternalConfigure(self, params): self.return_frequency = reader.read_param(params, 'return_frequency', True) self.molecular_final_states = reader.read_param(params, 'molecular_final_states', False) + # will be replaced with complex lineshape object if detailed lineshape is used self.complexLineShape = None @@ -129,51 +184,77 @@ def InternalConfigure(self, params): if self.use_lineshape: self.lineshape = self.detailed_or_simplified_lineshape if self.lineshape == 'simplified': - self.SimpParams = self.load_simp_params(self.scattering_sigma, + self.ls_params = self.load_simp_params(self.scattering_sigma, self.survival_prob, self.NScatters) elif self.lineshape=='detailed': # check path exists - if 'scatter_spectra_file' in self.detailed_scatter_spectra_path: - full_path = self.detailed_scatter_spectra_path - self.detailed_scatter_spectra_path, _ = os.path.split(full_path) + if 'scatter_spectra_file' in self.path_to_scatter_spectra_file: + full_path = self.path_to_scatter_spectra_file + self.path_to_scatter_spectra_file, _ = os.path.split(full_path) else: - full_path = os.path.join(self.detailed_scatter_spectra_path, 'scatter_spectra_file') + full_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra_file') - logger.info('Path to scatter_spectra_file: {}'.format(self.detailed_scatter_spectra_path)) + logger.info('Path to scatter_spectra_file: {}'.format(self.path_to_scatter_spectra_file)) # lineshape params - self.SimpParams = [self.scattering_sigma*2*math.sqrt(2*math.log(2)), self.survival_prob] + if self.resolution_function == 'gaussian_resolution' or self.resolution_function == 'gaussian': + self.ls_params = [self.scattering_sigma*2*math.sqrt(2*math.log(2)), self.survival_prob] + else: + self.ls_params = [self.scale_factor, self.survival_prob] # Setup and configure lineshape processor complexLineShape_config = { - 'gases': ["H2","He"], + 'gases': self.gases, 'max_scatters': self.NScatters, - 'fix_scatter_proportion': True, - # When fix_scatter_proportion is True, set the scatter proportion for gas1 below - 'gas1_scatter_proportion': self.scatter_proportion, - # This is an important parameter which determines how finely resolved - # the scatter calculations are. 10000 seems to produce a stable fit with minimal slowdown, for ~4000 fake events. The parameter may need to - # be increased for larger datasets. - 'num_points_in_std_array': 10000, - 'B_field': self.B_field, + 'trap_weights': self.trap_weights, + 'fixed_scatter_proportion': True, + # When fix_scatter_proportion is True, set the scatter proportion for the gases below + 'gas_scatter_proportion': self.scatter_proportion, + 'partially_fixed_scatter_proportion': False, + 'fixed_survival_probability': True, + 'survival_prob': self.survival_prob, + 'use_radiation_loss': self.use_radiation_loss, + 'sample_ins_res_errors': self.sample_ins_resolution_errors, + 'resolution_function': self.resolution_function, + 'scatter_peak_ratio_p': self.scatter_peak_ratio_p, + 'scatter_peak_ratio_q': self.scatter_peak_ratio_q, + 'factor': self.scatter_peak_ratio_factor, + 'fit_recon_eff': self.fit_recon_eff, + + #For analytics resolution functions, only: + 'ratio_gamma_to_sigma': self.ratio_gamma_to_sigma, + 'gaussian_proportion': self.gaussian_proportion, + 'A_array': self.A_array, + 'sigma_array': self.sigma_array, + + # This is an important parameter which determines how finely resolved the scatter calculations are. 10000 seems to produce a stable fit with minimal slowdown, for ~4000 fake events. The parameter may need to be increased for larger datasets. + 'num_points_in_std_array': 10000,#35846, 'base_shape': 'dirac', - 'path_to_osc_strengths_files': self.detailed_scatter_spectra_path + 'path_to_osc_strengths_files': self.path_to_osc_strengths_files, + 'path_to_scatter_spectra_file':self.path_to_scatter_spectra_file, + 'rad_loss_path': self.rad_loss_path, + 'path_to_ins_resolution_data_txt': self.path_to_ins_resolution_data_txt, + 'use_combined_four_trap_inst_reso': self.use_combined_four_trap_inst_reso, + 'path_to_four_trap_ins_resolution_data_txt': self.path_to_four_trap_ins_resolution_data_txt, + 'shake_spectrum_parameters_json_path': self.shake_spectrum_parameters_json_path } logger.info('Setting up complex lineshape object') - self.complexLineShape = KrComplexLineShape("complexLineShape") + self.complexLineShape = MultiGasComplexLineShape("complexLineShape") logger.info('Configuring complex lineshape') self.complexLineShape.Configure(complexLineShape_config) logger.info('Checking existence of scatter spectra files') self.complexLineShape.check_existence_of_scatter_file() + lineshape_array = self.complexLineShape.std_eV_array() + self.lineshape_stepize = lineshape_array[1]-lineshape_array[0] else: logger.error("'detailed_or_simplified' is neither 'detailed' nor 'simplified'") return False else: self.lineshape = 'gaussian' - self.SimpParams = [self.broadening] + self.ls_params = [self.scattering_sigma] logger.info('Lineshape is Gaussian') # check final states file existence @@ -206,7 +287,7 @@ def InternalRun(self): self.S, self.B_1kev, nsteps=self.n_steps, lineshape=self.lineshape, - params=self.SimpParams, + params=self.ls_params, efficiency_dict = self.efficiency_dict, err_from_B=self.err_from_B, B_field=self.B_field) @@ -262,7 +343,7 @@ def generate_unbinned_data(self, Q_mean, mass, ROIbound, S, B_1kev, nsteps=10**4 - 'gaussian' - 'simplified': Central gaussian + approximated scattering - 'detailed': Central gaussian + detailed scattering - 'params' is a list of the params inputted into the lineshape function. The first entry of the list should be a standard deviation of full width half max that provides the scale of the lineshape width. + 'params' is a list of the params inputted into the lineshape function. If such a parameter exists, the first entry of the list should be a standard deviation of full width half max that provides the scale of the lineshape width. """ logger.info('Going to generate pseudo-unbinned data with {} lineshape'.format(lineshape)) @@ -282,12 +363,8 @@ def generate_unbinned_data(self, Q_mean, mass, ROIbound, S, B_1kev, nsteps=10**4 nstdevs = 7 #Number of standard deviations (of size broadening) below Kmin and above Q-m to generate data, for the gaussian case FWHM_convert = 2*math.sqrt(2*math.log(2)) - if lineshape=='gaussian': - max_energy = nstdevs*params[0] - min_energy = self.min_energy - elif lineshape=='simplified_scattering' or lineshape=='simplified' or lineshape=='detailed_scattering' or lineshape=='detailed': - max_energy = nstdevs/FWHM_convert*params[0] - min_energy = self.min_energy + max_energy = -self.min_energy + min_energy = self.min_energy Kmax_eff = Kmax+max_energy #Maximum energy for data is slightly above Kmax>Q-m Kmin_eff = Kmin+min_energy #Minimum is slightly below Kminself.channel_bounds[i]] + temp_probsB = temp_probsB[Frequency(temp_Koptions, self.B_field)>self.channel_bounds[i]] + temp_Koptions = temp_Koptions[Frequency(temp_Koptions, self.B_field)>self.channel_bounds[i]] + + split_Koptions.append(temp_Koptions) + split_probsS.append(temp_probsS) + split_probsB.append(temp_probsB) + + rates = [] + for i in range(len(self.channel_runtimes)): + rates.append((S*runtime_ratios[i]*split_probsS[i] + B*split_probsB[i])/(S*runtime_ratios[i]+B)) + + self.Koptions = np.concatenate(split_Koptions) + rates = np.concatenate(rates) + self.probs = rates/np.sum(rates) + if self.poisson_stats: KE = np.random.choice(self.Koptions, np.random.poisson(S+B), p = self.probs) else: KE = np.random.choice(self.Koptions, round(S+B), p = self.probs) + time5 = time.time() logger.info('... took {} s'.format(time5-time4)) diff --git a/mermithid/processors/TritiumSpectrum/__init__.py b/mermithid/processors/TritiumSpectrum/__init__.py index cb5da878..1ffc9520 100644 --- a/mermithid/processors/TritiumSpectrum/__init__.py +++ b/mermithid/processors/TritiumSpectrum/__init__.py @@ -9,4 +9,5 @@ from .DistortedTritiumSpectrumLikelihoodSampler import * from .TritiumSpectrumProcessor import * from .FakeDataGenerator import FakeDataGenerator +from .BinnedTritiumMLFitter import * diff --git a/mermithid/processors/misc/KrComplexLineShape.py b/mermithid/processors/misc/KrComplexLineShape.py index b273ad90..0e39e416 100644 --- a/mermithid/processors/misc/KrComplexLineShape.py +++ b/mermithid/processors/misc/KrComplexLineShape.py @@ -54,7 +54,7 @@ def InternalConfigure(self, params): self.fix_scatter_proportion = reader.read_param(params, 'fix_scatter_proportion', True) if self.fix_scatter_proportion == True: self.scatter_proportion = reader.read_param(params, 'gas1_scatter_proportion', 0.8) - logger.info('Using an H2 scatter proportion of {} with gases {}'.format(self.gases, self.scatter_proportion)) + logger.info('Using an H2 scatter proportion of {} with gases {}'.format(self.scatter_proportion, self.gases)) # This is an important parameter which determines how finely resolved # the scatter calculations are. 10000 seems to produce a stable fit, with minimal slowdown self.num_points_in_std_array = reader.read_param(params, 'num_points_in_std_array', 10000) @@ -63,6 +63,7 @@ def InternalConfigure(self, params): self.shake_spectrum_parameters_json_path = reader.read_param(params, 'shake_spectrum_parameters_json_path', 'shake_spectrum_parameters.json') self.base_shape = reader.read_param(params, 'base_shape', 'shake') self.path_to_osc_strengths_files = reader.read_param(params, 'path_to_osc_strengths_files', '/host/') + self.path_to_ins_resolution_data_txt = reader.read_param(params, 'path_to_ins_resolution_data_txt', '/termite/analysis_input/complex-lineshape-inputs/res_all_conversion_max15.5_alltraps.txt') if self.base_shape=='shake' and not os.path.exists(self.shake_spectrum_parameters_json_path): raise IOError('Shake spectrum path does not exist') @@ -250,8 +251,28 @@ def convolve_gaussian(self, func_to_convolve,gauss_FWHM_eV): ans = signal.convolve(resolution_f,func_to_convolve,mode='same') ans_normed = self.normalize(ans) return ans_normed - - def make_spectrum(self, gauss_FWHM_eV, prob_parameter, scatter_proportion, emitted_peak='shake'): + + def read_ins_resolution_data(self, path_to_ins_resolution_data_txt): + ins_resolution_data = np.loadtxt(path_to_ins_resolution_data_txt) + x_data = ins_resolution_data.T[0] + y_data = ins_resolution_data.T[1] + y_err_data = ins_resolution_data.T[2] + return x_data, y_data, y_err_data + + def convolve_ins_resolution(self, working_spectrum): + x_data, y_mean_data, y_err_data = self.read_ins_resolution_data(self.path_to_ins_resolution_data_txt) + y_data = np.random.normal(y_mean_data, y_err_data) + y_data[y_data<0] = 0 + f = interpolate.interp1d(x_data, y_data) + x_array = self.std_eV_array() + y_array = np.zeros(len(x_array)) + index_within_range_of_xdata = np.where((x_array >= x_data[0]) & (x_array <= x_data[-1])) + y_array[index_within_range_of_xdata] = f(x_array[index_within_range_of_xdata]) + convolved_spectrum = signal.convolve(working_spectrum, y_array, mode = 'same') + normalized_convolved_spectrum = self.normalize(convolved_spectrum) + return normalized_convolved_spectrum + + def make_spectrum(self, prob_parameter, scatter_proportion, emitted_peak='shake'): gases = self.gases max_scatters = self.max_scatters max_comprehensive_scatters = self.max_comprehensive_scatters @@ -271,7 +292,7 @@ def make_spectrum(self, gauss_FWHM_eV, prob_parameter, scatter_proportion, emitt current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() elif emitted_peak == 'dirac': current_working_spectrum = self.std_dirac() - current_working_spectrum = self.convolve_gaussian(current_working_spectrum, gauss_FWHM_eV) + current_working_spectrum = self.convolve_ins_resolution(current_working_spectrum) zeroth_order_peak = current_working_spectrum current_full_spectrum += current_working_spectrum for n in range(1, max_comprehensive_scatters + 1): @@ -312,18 +333,17 @@ def spectrum_func(self, x_keV, *p0): f = np.zeros(len(x_keV)) f_intermediate = np.zeros(len(x_keV)) - FWHM_G_eV = p0[0] - line_pos_keV = p0[1] - amplitude = p0[2] - prob_parameter = p0[3] - scatter_proportion = p0[4] + line_pos_keV = p0[0] + amplitude = p0[1] + prob_parameter = p0[2] + scatter_proportion = p0[3] line_pos_eV = line_pos_keV*1000. x_eV_minus_line = x_eV - line_pos_eV zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] nonzero_idx = [i for i in range(len(x_keV)) if i not in zero_idx] - full_spectrum = self.make_spectrum(FWHM_G_eV, prob_parameter, scatter_proportion) + full_spectrum = self.make_spectrum(prob_parameter, scatter_proportion) full_spectrum_rev = ComplexLineShapeUtilities.flip_array(full_spectrum) f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx],en_array_rev,full_spectrum_rev) f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) @@ -358,7 +378,6 @@ def fit_data(self, freq_bins, data_hist_freq, print_params=True): scatter_proportion_min = 1e-5 scatter_proportion_max = 1 # Initial guesses for curve_fit - FWHM_guess = 5 line_pos_guess = bins_keV[np.argmax(data_hist)] amplitude_guess = np.sum(data_hist)/2 prob_parameter_guess = 0.5 @@ -434,7 +453,7 @@ def fit_data(self, freq_bins, data_hist_freq, print_params=True): } return dictionary_of_fit_results - def make_spectrum_1(self, gauss_FWHM_eV, prob_parameter, emitted_peak='shake'): + def make_spectrum_1(self, prob_parameter, emitted_peak='shake'): gases = self.gases max_scatters = self.max_scatters max_comprehensive_scatters = self.max_comprehensive_scatters @@ -454,7 +473,7 @@ def make_spectrum_1(self, gauss_FWHM_eV, prob_parameter, emitted_peak='shake'): current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() elif emitted_peak == 'dirac': current_working_spectrum = self.std_dirac() - current_working_spectrum = self.convolve_gaussian(current_working_spectrum, gauss_FWHM_eV) + current_working_spectrum = self.convolve_ins_resolution(current_working_spectrum) zeroth_order_peak = current_working_spectrum current_full_spectrum += current_working_spectrum for n in range(1, max_comprehensive_scatters + 1): @@ -492,17 +511,16 @@ def spectrum_func_1(self, x_keV, *p0): f = np.zeros(len(x_keV)) f_intermediate = np.zeros(len(x_keV)) - FWHM_G_eV = p0[0] - line_pos_keV = p0[1] - amplitude = p0[2] - prob_parameter = p0[3] + line_pos_keV = p0[0] + amplitude = p0[1] + prob_parameter = p0[2] line_pos_eV = line_pos_keV*1000. x_eV_minus_line = x_eV - line_pos_eV zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] nonzero_idx = [i for i in range(len(x_keV)) if i not in zero_idx] - full_spectrum = self.make_spectrum_1(FWHM_G_eV, prob_parameter,emitted_peak=self.base_shape) + full_spectrum = self.make_spectrum_1(prob_parameter, emitted_peak=self.base_shape) full_spectrum_rev = ComplexLineShapeUtilities.flip_array(full_spectrum) f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx],en_array_rev,full_spectrum_rev) f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) diff --git a/mermithid/processors/misc/MultiGasComplexLineShape.py b/mermithid/processors/misc/MultiGasComplexLineShape.py new file mode 100644 index 00000000..4f37a0d9 --- /dev/null +++ b/mermithid/processors/misc/MultiGasComplexLineShape.py @@ -0,0 +1,3749 @@ +''' +Fits data to complex lineshape model. +Author: E. Machado, Y.-H. Sun, E. Novitski, T. Weiss, X. Huyan +Date: 2/9/2021 + +This processor takes in frequency data in binned histogram and fit the histogram with two gas scattering complex line shape model. + +Configurable parameters: + +There are two options available for fitting: fix_scatter_proportion = True and False. +gases: array for names of the two gases involved in the scattering process. +max_scatter: max number of scatterings for only single gas scatterings. +max_comprehansive_scatter: max number of scatterings for all cross scatterings. +scatter_proportion: when fix_scatter_proportion is set as true, gives the fixed scatter proportion. +num_points_in_std_array: number of points for std_array defining how finely the scatter calculations are. +RF_ROI_MIN: can be found from meta data. +B_field: can be put in hand or found by position of the peak of the frequency histogram. +shake_spectrum_parameters_json_path: path to json file storing shake spectrum parameters. +path_to_osc_strength_files: path to oscillator strength files. +''' + +from __future__ import absolute_import + +import numpy as np +from scipy.optimize import curve_fit +from scipy.special import comb +from scipy import integrate , signal, interpolate +from itertools import product +from math import factorial +from iminuit import Minuit +import os +import time +import sys +from morpho.utilities import morphologging, reader +from morpho.processors import BaseProcessor +from mermithid.misc import Constants, ComplexLineShapeUtilities, ConversionFunctions + +logger = morphologging.getLogger(__name__) + + + +__all__ = [] +__all__.append(__name__) + +class MultiGasComplexLineShape(BaseProcessor): + + def InternalConfigure(self, params): + ''' + Configure + ''' + # Read other parameters + self.bins_choice = reader.read_param(params, 'bins_choice', []) + self.gases = reader.read_param(params, 'gases', ["H2", "Kr", "He", "Ar"]) + # when self.fix_gas_composition and self.fix_width_scale_factor are both True, + # fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor is used, + # Then the first N-1 gas compositions are set below through self.scatter_fractions_for_gases + # Otherwise, fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio is used + self.fix_gas_composition = reader.read_param(params, 'fix_gas_composition', False) + self.fix_width_scale_factor = reader.read_param(params, 'fix_width_scale_factor', False) + self.scatter_fractions_for_gases = reader.read_param(params, 'scatter_fractions_for_gases', []) + self.max_scatters = reader.read_param(params, 'max_scatters', 20) + self.trap_weights = reader.read_param(params, 'trap_weights', {'weights':[0.076, 0.341, 0.381, 0.203], 'errors':[0.003, 0.013, 0.014, 0.02]}) #Weights from Xueying's Sept. 13 slides; errors currently arbitrary + self.fixed_scatter_proportion = reader.read_param(params, 'fixed_scatter_proportion', True) + if self.fixed_scatter_proportion == True: + self.scatter_proportion = reader.read_param(params, 'gas_scatter_proportion', []) + self.partially_fixed_scatter_proportion = reader.read_param(params, 'partially_fixed_scatter_proportion', True) + if self.partially_fixed_scatter_proportion == True: + self.free_gases = reader.read_param(params, 'free_gases', ["H2", "He"]) + self.fixed_gases = reader.read_param(params, 'fixed_gases', ["Ar", "Kr"]) + self.gases = self.free_gases + self.fixed_gases + self.scatter_proportion_for_fixed_gases = reader.read_param(params, 'scatter_proportion_for_fixed_gases', [0.018, 0.039]) + self.fixed_survival_probability = reader.read_param(params, 'fixed_survival_probability', True) + if self.fixed_survival_probability == True: + self.survival_prob = reader.read_param(params, 'survival_prob', 1) + self.use_radiation_loss = reader.read_param(params, 'use_radiation_loss', True) + self.sample_ins_resolution_errors = reader.read_param(params, 'sample_ins_res_errors', False) + # configure the resolution functions: gaussian_lorentzian_composite_resolution, elevated_gaussian, composite_gaussian, composite_gaussian_pedestal_factor, and simulated_resolution_scaled + self.resolution_function = reader.read_param(params, 'resolution_function', '') + if self.resolution_function == 'gaussian_lorentzian_composite_resolution': + self.ratio_gamma_to_sigma = reader.read_param(params, 'ratio_gamma_to_sigma', 0.8) + self.gaussian_proportion = reader.read_param(params, 'gaussian_proportion', 0.8) + if self.resolution_function == 'elevated_gaussian': + self.ratio_gamma_to_sigma = reader.read_param(params, 'ratio_gamma_to_sigma', 0.8) + if self.resolution_function == 'composite_gaussian' or 'composite_gaussian_pedestal_factor': + self.A_array = reader.read_param(params, 'A_array', [0.076, 0.341, 0.381, 0.203]) + self.sigma_array = reader.read_param(params, 'sigma_array', [5.01, 13.33, 15.40, 11.85]) + if self.resolution_function == 'simulated_resolution_scaled': + self.fit_recon_eff = reader.read_param(params, 'fit_recon_eff', False) + if self.resolution_function == 'simulated_resolution_scaled_fit_scatter_peak_ratio' or 'gaussian_resolution_fit_scatter_peak_ratio': + self.fixed_parameter_names = reader.read_param(params, 'fixed_parameter_names', []) + self.fixed_parameter_values = reader.read_param(params, 'fixed_parameter_values', []) + #self.elevation_factor = reader.read_param(params, 'elevation_factor', 20) + # This is an important parameter which determines how finely resolved + # the scatter calculations are. 10000 seems to produce a stable fit, with minimal slowdown + self.num_points_in_std_array = reader.read_param(params, 'num_points_in_std_array', 10000) + self.RF_ROI_MIN = reader.read_param(params, 'RF_ROI_MIN', 25850000000.0) + self.base_shape = reader.read_param(params, 'base_shape', 'shake') + self.shake_spectrum_parameters_json_path = reader.read_param(params, 'shake_spectrum_parameters_json_path', 'shake_spectrum_parameters.json') + self.path_to_osc_strengths_files = reader.read_param(params, 'path_to_osc_strengths_files', '/host/') + self.path_to_scatter_spectra_file = reader.read_param(params, 'path_to_scatter_spectra_file', '/host/') + self.path_to_missing_track_radiation_loss_data_numpy_file = reader.read_param(params, 'rad_loss_path', '/host') + self.path_to_ins_resolution_data_txt = reader.read_param(params, 'path_to_ins_resolution_data_txt', '/host/res_cf15.5_all.txt') + self.use_combined_four_trap_inst_reso = reader.read_param(params, 'use_combined_four_trap_inst_reso', False) + self.path_to_four_trap_ins_resolution_data_txt = reader.read_param(params, 'path_to_four_trap_ins_resolution_data_txt', ['/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap1.txt', '/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap2.txt', '/host/T2-1.56e-4/analysis_input/complex-lineshape-inputs/res_cf15.5_trap3.txt', '/host/analysis_input/complex-lineshape-inputs/T2-1.56e-4/res_cf15.5_trap4.txt']) + self.use_quad_trap_eff_interp = reader.read_param(params, 'use_quad_trap_eff_interp', True) + if self.use_quad_trap_eff_interp == True: + self.path_to_quad_trap_eff_interp = reader.read_param(params, 'path_to_quad_trap_eff_interp', '/host/quad_interps.npy') + self.recon_eff_params = reader.read_param(params, 'recon_eff_params', [0.005569990343215976, 0.351, 0.546]) + self.recon_eff_param_a = self.recon_eff_params[0] + self.recon_eff_param_b = self.recon_eff_params[1] + self.recon_eff_param_c = self.recon_eff_params[2] + self.factor = reader.read_param(params, 'factor', []) + + if not os.path.exists(self.shake_spectrum_parameters_json_path) and self.base_shape=='shake': + raise IOError('Shake spectrum path does not exist') + if not os.path.exists(self.path_to_osc_strengths_files): + raise IOError('Path to osc strengths files does not exist') + # Read shake parameters from JSON file + if self.base_shape == 'shake': + self.shakeSpectrumClassInstance = ComplexLineShapeUtilities.ShakeSpectrumClass(self.shake_spectrum_parameters_json_path, self.std_eV_array()) + + # read in resolution if simulated + if 'simulated' in self.resolution_function: + self.sample_and_interpolate_resolution() + return True + + def InternalRun(self): + + # number_of_events = len(self.data['StartFrequency']) + # self.results = number_of_events + + a = self.data['StartFrequency'] + + # fit with shake spectrum + data_hist_freq, freq_bins= np.histogram(a,bins=self.bins_choice) + # histogram = data_hist_freq +# bins = freq_bins +# guess = np.where(np.array(histogram) == np.max(histogram))[0][0] +# kr17kev_in_hz = guess*(bins[1]-bins[0])+bins[0] + #self.B_field = B(17.8, kr17kev_in_hz + 0) + if self.resolution_function == 'simulated_resolution': + if self.fixed_scatter_proportion == True: + self.results = self.fit_data_ftc(freq_bins, data_hist_freq) + else: + self.results = self.fit_data_ftc_2(freq_bins, data_hist_freq) + elif self.resolution_function == 'gaussian_resolution': + if self.fixed_scatter_proportion == True: + self.results = self.fit_data_1(freq_bins, data_hist_freq) + else: + self.results = self.fit_data(freq_bins, data_hist_freq) + elif self.resolution_function == 'gaussian_lorentzian_composite_resolution': + if self.fixed_scatter_proportion == True and self.fixed_survival_probability == True: + self.results = self.fit_data_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(freq_bins, data_hist_freq) + elif self.fixed_scatter_proportion == True and self.fixed_survival_probability == False: + self.results = self.fit_data_composite_gaussian_lorentzian_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.fixed_scatter_proportion == False and self.fixed_survival_probability == True and self.partially_fixed_scatter_proportion == False: + self.results = self.fit_data_composite_gaussian_lorentzian_fixed_survival_probability(freq_bins, data_hist_freq) + elif self.partially_fixed_scatter_proportion == True and self.fixed_survival_probability == True: + self.results = self.fit_data_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.resolution_function == 'elevated_gaussian': + self.results = self.fit_data_elevated_gaussian_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.resolution_function == 'composite_gaussian': + self.results = self.fit_data_composite_gaussian_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.resolution_function == 'composite_gaussian_pedestal_factor': + self.results = self.fit_data_composite_gaussian_pedestal_factor_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.resolution_function == 'composite_gaussian_scaled': + self.results = self.fit_data_composite_gaussian_scaled_fixed_scatter_proportion(freq_bins, data_hist_freq) + elif self.resolution_function == 'simulated_resolution_scaled': + if self.fit_recon_eff == False: + self.results = self.fit_data_simulated_resolution_scaled_fixed_scatter_proportion(freq_bins, data_hist_freq) + else: + self.results = self.fit_data_simulated_resolution_scaled_fit_recon_eff(freq_bins, data_hist_freq) + elif self.resolution_function == 'simulated_resolution_scaled_fit_scatter_peak_ratio': + if self.fix_gas_composition == True and self.fix_width_scale_factor == True: + self.results = self.fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(freq_bins, data_hist_freq) + else: + self.results = self.fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio(freq_bins, data_hist_freq) + elif self.resolution_function == 'gaussian_resolution_fit_scatter_peak_ratio': + self.results = self.fit_data_gaussian_resolution_fit_scatter_peak_ratio(freq_bins, data_hist_freq) + return True + + + # Establishes a standard energy loss array (SELA) from -1000 eV to 1000 eV + # with number of points equal to self.num_points_in_std_array. All convolutions + # will be carried out on this particular discretization + def std_eV_array(self): + emin = -1000 + emax = 1000 + array = np.linspace(emin,emax,self.num_points_in_std_array) + return array + + # A lorentzian line centered at 0 eV, with 2.83 eV width on the SELA + def std_lorenztian_17keV(self): + x_array = self.std_eV_array() + ans = lorentzian(x_array,0,kr_line_width) + return ans + + #A Dirac delta functin + def std_dirac(self): + x_array = self.std_eV_array() + ans = np.zeros(len(x_array)) + min_x = np.min(np.abs(x_array)) + ans[np.abs(x_array)==min_x] = 1. + logger.warning('Spectrum will be shifted by lineshape by {} eV'.format(min_x)) + if min_x > 0.1: + logger.warning('Lineshape will shift spectrum by > 0.1 eV') + if min_x > 1.: + logger.warning('Lineshape will shift spectrum by > 1 eV') + raise ValueError('problem with std_eV_array()') + return ans + + # A gaussian function + def gaussian(self, x_array, A, sigma, mu): + f = A*(1./(sigma*np.sqrt(2*np.pi)))*np.exp(-(((x_array-mu)/sigma)**2.)/2.) + return f + + # A gaussian centered at 0 eV with variable width, on the SELA + def std_gaussian(self, sigma): + x_array = self.std_eV_array() + ans = ComplexLineShapeUtilities.gaussian(x_array,1,sigma,0) + return ans + + def composite_gaussian(self): + x_array = self.std_eV_array() + ans = 0 + A_array = self.A_array + sigma_array = self.sigma_array + for A, sigma in zip(A_array, sigma_array): + ans += self.gaussian(x_array, A, sigma, 0) + return ans + + def composite_gaussian_pedestal_factor(self, pedestal_factor): + x_array = self.std_eV_array() + ans = 0 + A_array = self.A_array + sigma_array = np.array(self.sigma_array) + sigma_array = sigma_array*[1, 1, pedestal_factor, 1] + for A, sigma in zip(A_array, sigma_array): + ans += self.gaussian(x_array, A, sigma, 0) + return ans + + def composite_gaussian_scaled(self, scale_factor): + x_array = self.std_eV_array() + ans = 0 + A_array = self.A_array + sigma_array = np.array(self.sigma_array) + sigma_array = sigma_array*scale_factor + for A, sigma in zip(A_array, sigma_array): + ans += self.gaussian(x_array, A, sigma, 0) + return ans + + def asym_triangle(self, x, scale1, scale2, center, exponent=1): + index_below = np.where(x=center) + f_below = 1-np.abs((x-center)/scale1)**exponent + f_above = 1-np.abs((x-center)/scale2)**exponent + f_below[np.abs(x-center)>=np.abs(scale1)]=0. + f_above[np.abs(x-center)>=np.abs(scale2)]=0. + f = np.zeros(len(x)) + f[index_above] = f_above[index_above] + f[index_below] = f_below[index_below] + return f + + def smeared_triangle(self, x, center, scale1, scale2, exponent, sigma, amplitude): + max_energy = 1000 + dx = x[1]-x[0]#E[1]-E[0] + n_dx = round(max_energy/dx) + x_smearing = np.arange(-n_dx*dx, n_dx*dx, dx) + x_triangle = np.arange(min(x)-max_energy, max(x)+max_energy, dx) + smearing = gaussian(x_smearing, 1, sigma, 0) + triangle = asym_triangle(x_triangle, scale1, scale2, center, exponent) + triangle_smeared = signal.convolve(triangle, smearing, mode='same') + triangle_smeared_norm = triangle_smeared/np.sum(triangle_smeared)*amplitude + return np.interp(x, x_triangle, triangle_smeared_norm) + + def std_smeared_triangle(self, center, scale1, scale2, exponent, sigma): + x_array = std_eV_array() + ans = self.smeared_triangle(x_array, center, scale1, scale2, exponent, sigma, 1) + return ans + + def composite_gaussian_lorentzian(self, sigma): + x_array = self.std_eV_array() + w_g = x_array/sigma + gamma = self.ratio_gamma_to_sigma*sigma + w_l = x_array/gamma + lorentzian = 1./(gamma*np.pi)*1./(1+(w_l**2)) + gaussian = 1./(np.sqrt(2.*np.pi)*sigma)*np.exp(-0.5*w_g**2) + p = self.gaussian_proportion + composite_function = p*gaussian+(1-p)*lorentzian + return composite_function + + def elevated_gaussian(self, elevation_factor, sigma): + x_array = self.std_eV_array() + w_g = x_array/sigma + gamma = self.ratio_gamma_to_sigma*sigma + w_l = x_array/gamma + lorentzian = 1./(gamma*np.pi)*1./(1+(w_l**2)) + gaussian = 1./(np.sqrt(2.*np.pi)*sigma)*np.exp(-0.5*w_g**2) + modified_guassian_function = gaussian*(1 + elevation_factor*lorentzian) + return modified_guassian_function + + # normalizes a function, but depends on binning. + # Only to be used for functions evaluated on the SELA + def normalize(self, f): + x_arr = self.std_eV_array() + f_norm = integrate.simps(f,x=x_arr) + f_normed = f/f_norm + return f_normed + + # Function for energy loss from a single scatter of electrons by + # V.N. Aseev et al. 2000 + # This function does the work of combining fit_func1 and fit_func2 by + # finding the point where they intersect. + # Evaluated on the SELA + def single_scatter_f(self, gas_type): + energy_loss_array = self.std_eV_array() + f = 0 * energy_loss_array + + input_filename = self.path_to_osc_strengths_files + gas_type + "OscillatorStrength.txt" + energy_fOsc = ComplexLineShapeUtilities.read_oscillator_str_file(input_filename) + fData = interpolate.interp1d(energy_fOsc[0], energy_fOsc[1], kind='linear') + for i in range(len(energy_loss_array)): + if energy_loss_array[i] < energy_fOsc[0][0]: + f[i] = 0 + elif energy_loss_array[i] <= energy_fOsc[0][-1]: + f[i] = fData(energy_loss_array[i]) + else: + f[i] = ComplexLineShapeUtilities.aseev_func_tail(energy_loss_array[i], gas_type) + + f_e_loss = ComplexLineShapeUtilities.get_eloss_spec(energy_loss_array, f, Constants.kr_k_line_e()) + f_normed = self.normalize(f_e_loss) + return f_normed + + # Convolves a function with the single scatter function, on the SELA + def another_scatter(self, input_spectrum, gas_type): + single = self.single_scatter_f(gas_type) + f = signal.convolve(single,input_spectrum,mode='same') + f_normed = self.normalize(f) + return f_normed + + def radiation_loss_f(self): + radiation_loss_data_file_path = self.path_to_missing_track_radiation_loss_data_numpy_file + '/missing_track_radiation_loss.npy' + data_for_missing_track_radiation_loss = np.load(radiation_loss_data_file_path, allow_pickle = True) + x_data_for_histogram = data_for_missing_track_radiation_loss.item()['histogram_eV']['x_data'] + energy_loss_array = self.std_eV_array() + f_radiation_energy_loss = 0 * energy_loss_array + f_radiation_energy_loss_interp = data_for_missing_track_radiation_loss.item()['histogram_eV']['interp'] + for i in range(len(energy_loss_array)): + if energy_loss_array[i] >= x_data_for_histogram[0] and energy_loss_array[i] <= x_data_for_histogram[-1]: + f_radiation_energy_loss[i] = f_radiation_energy_loss_interp(energy_loss_array[i]) + else: + f_radiation_energy_loss[i] = 0 + return f_radiation_energy_loss + + # Convolves the scatter functions and saves + # the results to a .npy file. + def generate_scatter_convolution_file(self): + t = time.time() + scatter_spectra_single_gas = {} + for gas_type in self.gases: + scatter_spectra_single_gas[gas_type] = {} + first_scatter = self.single_scatter_f(gas_type) + if self.use_radiation_loss == True: + f_radiation_loss = self.radiation_loss_f() + first_scatter = self.normalize(signal.convolve(first_scatter, f_radiation_loss, mode = 'same')) + scatter_num_array = range(2, self.max_scatters+1) + current_scatter = first_scatter + scatter_spectra_single_gas[gas_type][str(1).zfill(2)] = current_scatter + # x = std_eV_array() # diagnostic + for i in scatter_num_array: + current_scatter = self.another_scatter(current_scatter, gas_type) + if self.use_radiation_loss == True: + f_radiation_loss = self.radiation_loss_f() + current_scatter = self.normalize(signal.convolve(current_scatter, f_radiation_loss, mode = 'same')) + scatter_spectra_single_gas[gas_type][str(i).zfill(2)] = current_scatter + N = len(self.gases) + scatter_spectra = {} + for M in range(1, self.max_scatters + 1): + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + mark_first_nonzero_component = 0 + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + if component == 0: + continue + else: + if mark_first_nonzero_component == 0: + current_full_scatter = scatter_spectra_single_gas[gas_type][str(component).zfill(2)] + mark_first_nonzero_component = 1 + else: + scatter_to_add = scatter_spectra_single_gas[gas_type][str(component).zfill(2)] + current_full_scatter = self.normalize(signal.convolve(current_full_scatter, scatter_to_add, mode='same')) + scatter_spectra[entry_str] = current_full_scatter + np.save(os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy'), scatter_spectra) + elapsed = time.time() - t + logger.info('Files generated in '+str(elapsed)+'s') + return + + # Checks for the existence of a directory called 'scatter_spectra_file' + # and checks that this directory contains the scatter spectra files. + # If not, this function calls generate_scatter_convolution_file. + # This function also checks to make sure that the scatter file have the correct + # number of entries and correct number of points in the SELA, and if not, it generates a fresh file. + # When the variable regenerate is set as True, it generates a fresh file + def check_existence_of_scatter_file(self, regenerate = True): + gases = self.gases + if regenerate == True: + logger.info('generate fresh scatter file') + self.generate_scatter_convolution_file() + else: + directory = os.listdir(self.path_to_scatter_spectra_file) + strippeddirs = [s.strip('\n') for s in directory] + if 'scatter_spectra.npy' not in strippeddirs: + self.generate_scatter_convolution_file() + test_file = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + test_dict = np.load(test_file, allow_pickle = True) + N = len(self.gases) + if len(test_dict.item()) != sum([comb(M + N -1, N -1) for M in range(1, self.max_scatters+1)]): + logger.info('Number of scatter combinations not matching, generating fresh files') + self.generate_scatter_convolution_file() + test_dict = np.load(test_file, allow_pickle = True) + gas_str = gases[0] + '01' + for gas in self.gases[1:]: + gas_str += gas + '00' + if gas_str not in list(test_dict.item().keys()): + print('Gas species not matching, generating fresh files') + self.generate_scatter_convolution_files() + return + + # Given a function evaluated on the SELA, convolves it with a gaussian + def convolve_gaussian(self, func_to_convolve, gauss_FWHM_eV): + sigma = ComplexLineShapeUtilities.gaussian_FWHM_to_sigma(gauss_FWHM_eV) + resolution_f = self.std_gaussian(sigma) + ans = signal.convolve(resolution_f, func_to_convolve,mode='same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_composite_gaussian(self, func_to_convolve, A_array, sigma_array): + resolution_f = self.composite_gaussian(A_array, sigma_array) + ans = signal.convolve(resolution_f, func_to_convolve, mode='same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_composite_gaussian_lorentzian(self, func_to_convolve, sigma): + resolution_f = self.composite_gaussian_lorentzian(sigma) + ans = signal.convolve(resolution_f, func_to_convolve, mode='same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_elevated_gaussian(self, func_to_convolve, elevation_factor, sigma): + resolution_f = self.elevated_gaussian(elevation_factor, sigma) + ans = signal.convolve(resolution_f, func_to_convolve, mode = 'same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_composite_gaussian(self, func_to_convolve): + resolution_f = self.composite_gaussian() + ans = signal.convolve(resolution_f, func_to_convolve, mode = 'same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_composite_gaussian_pedestal_factor(self, func_to_convolve, pedestal_factor): + resolution_f = self.composite_gaussian_pedestal_factor(pedestal_factor) + ans = signal.convolve(resolution_f, func_to_convolve, mode = 'same') + ans_normed = self.normalize(ans) + return ans_normed + + def convolve_composite_gaussian_scaled(self, func_to_convolve, scale_factor): + resolution_f = self.composite_gaussian_scaled(scale_factor) + ans = signal.convolve(resolution_f, func_to_convolve, mode = 'same') + ans_normed = self.normalize(ans) + return ans_normed + + def read_ins_resolution_data(self, path_to_ins_resolution_data_txt): + ins_resolution_data = np.loadtxt(path_to_ins_resolution_data_txt) + x_data = ins_resolution_data.T[0] + y_data = ins_resolution_data.T[1] + y_err_data = ins_resolution_data.T[2] + x_data = ComplexLineShapeUtilities.flip_array(-1*x_data) + y_data = ComplexLineShapeUtilities.flip_array(y_data) + y_err_data = ComplexLineShapeUtilities.flip_array(y_err_data) + return x_data, y_data, y_err_data + + def convolve_ins_resolution(self, working_spectrum): + x_data, y_mean_data, y_err_data = self.read_ins_resolution_data(self.path_to_ins_resolution_data_txt) + if self.sample_ins_resolution_errors: + y_data = np.random.normal(y_mean_data, y_err_data) + else: + y_data = y_mean_data + y_data[y_data<0] = 0 + f = interpolate.interp1d(x_data, y_data) + x_array = self.std_eV_array() + y_array = np.zeros(len(x_array)) + index_within_range_of_xdata = np.where((x_array >= x_data[0]) & (x_array <= x_data[-1])) + y_array[index_within_range_of_xdata] = f(x_array[index_within_range_of_xdata]) + convolved_spectrum = signal.convolve(working_spectrum, y_array, mode = 'same') + normalized_convolved_spectrum = self.normalize(convolved_spectrum) + return normalized_convolved_spectrum + + def combine_four_trap_resolution_from_txt(self, trap_weights): + if self.sample_ins_resolution_errors: + weight_array = np.random.normal(trap_weights['weights'], trap_weights['errors']) + else: + weight_array = trap_weights['weights'] + y_data_array = [] + y_err_data_array = [] + for path_to_single_trap_resolution_txt in self.path_to_four_trap_ins_resolution_data_txt: + x_data, y_data, y_err_data = self.read_ins_resolution_data(path_to_single_trap_resolution_txt) + y_data_array.append(y_data) + y_err_data_array.append(y_err_data) + y_data_combined = weight_array[0]*y_data_array[0] + weight_array[1]*y_data_array[1] + weight_array[2]*y_data_array[2] + weight_array[3]*y_data_array[3] + y_err_data_combined = np.sqrt((weight_array[0]*y_err_data_array[0])**2 + (weight_array[1]*y_err_data_array[1])**2 + (weight_array[2]*y_err_data_array[2])**2 + (weight_array[3]*y_err_data_array[3])**2) + return x_data, y_data_combined, y_err_data_combined + + def convolve_ins_resolution_combining_four_trap(self, working_spectrum, weight_array): + x_data, y_data_combined, y_err_data_combined = self.combine_four_trap_resolution_from_txt(weight_array) + if self.sample_ins_resolution_errors: + y_data_combined = np.random.normal(y_data_combined, y_err_data_combined) + f = interpolate.interp1d(x_data, y_data_combined) + x_array = self.std_eV_array() + y_array = np.zeros(len(x_array)) + index_within_range_of_xdata = np.where((x_array >= x_data[0]) & (x_array <= x_data[-1])) + y_array[index_within_range_of_xdata] = f(x_array[index_within_range_of_xdata]) + convolved_spectrum = signal.convolve(working_spectrum, y_array, mode = 'same') + normalized_convolved_spectrum = self.normalize(convolved_spectrum) + return normalized_convolved_spectrum + + def convolve_simulated_resolution_scaled(self, working_spectrum, scale_factor): + """if self.use_combined_four_trap_inst_reso: + x_data, y_data, y_err_data = self.combine_four_trap_resolution_from_txt(self.trap_weights) + logger.info("Combined four instrumental resolution files") + else: + x_data, y_data, y_err_data = self.read_ins_resolution_data(self.path_to_ins_resolution_data_txt) + logger.info("Using ONE simulated instrumental resolution file (not combining four)") + if self.sample_ins_resolution_errors: + y_data = np.random.normal(y_data, y_err_data) + logger.info("Sampling instrumental resolution counts per bin") + scaled_xdata = x_data*scale_factor + f = interpolate.interp1d(x_data*scale_factor, y_data)""" + + x_array = self.std_eV_array() + y_array = np.zeros(len(x_array)) + + #index_within_range_of_xdata = np.where((x_array >= scaled_xdata[0]) & (x_array <= scaled_xdata[-1])) + #y_array[index_within_range_of_xdata] = f(x_array[index_within_range_of_xdata]/scale_factor) + y_array = self.interpolated_resolution(x_array/scale_factor) + convolved_spectrum = signal.convolve(working_spectrum, y_array, mode = 'same') + normalized_convolved_spectrum = self.normalize(convolved_spectrum) + return normalized_convolved_spectrum + + # do resolution sampling + def sample_and_interpolate_resolution(self): + if self.use_combined_four_trap_inst_reso: + x_data, y_data, y_err_data = self.combine_four_trap_resolution_from_txt(self.trap_weights) + logger.info("Combined four instrumental resolution files") + else: + x_data, y_data, y_err_data = self.read_ins_resolution_data(self.path_to_ins_resolution_data_txt) + logger.info("Using ONE simulated instrumental resolution file (not combining four)") + if self.sample_ins_resolution_errors: + y_data = np.random.normal(y_data, y_err_data) + logger.info("Sampling instrumental resolution counts per bin") + self.interpolated_resolution = interpolate.interp1d(x_data, y_data, fill_value=(0,0), bounds_error=False) + + #might be untested + def convolve_smeared_triangle(self, func_to_convolve, center, scale1, scale2, exponent, sigma): + resolution_f = self.std_smeared_triangle(center, scale1, scale2, exponent, sigma) + ans = signal.convolve(resolution_f, func_to_convolve, mode = 'same') + ans_normed = self.normalize(ans) + return ans_normed + + def least_square(self, bin_centers, hist, params): + # expectation + expectation = self.spectrum_func_ftc(bin_centers, *params) + + high_count_index = np.where(hist>0) + #low_count_index = np.where((hist>0) & (hist<=50)) + zero_count_index = np.where(hist==0) + + lsq = ((hist[high_count_index]- expectation[high_count_index])**2/hist[high_count_index]).sum() + #lsq += ((hist[low_count_index]- expectation[low_count_index])**2/hist[low_count_index]).sum() + lsq += ((hist[zero_count_index]- expectation[zero_count_index])**2).sum() + return lsq + + def chi2_Poisson(self, bin_centers, data_hist_freq, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + if self.resolution_function == 'simulated_resolution': + if self.fixed_scatter_proportion: + fit_Hz = self.spectrum_func_ftc(bin_centers, *params) + else: + fit_Hz = self.spectrum_func_ftc_2(bin_centers, *params) + if self.resolution_function == 'gaussian_resolution': + if self.fixed_scatter_proportion: + fit_Hz = self.spectrum_func_1(bin_centers, *params) + else: + fit_Hz = self.spectrum_func(bin_centers, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_composite_gaussian_lorentzian_reso(self, bin_centers, data_hist_freq, eff_array, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + if self.fixed_scatter_proportion == True and self.fixed_survival_probability == True: + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(bin_centers, eff_array, *params) + elif self.fixed_scatter_proportion == True and self.fixed_survival_probability == False: + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion(bin_centers, eff_array, *params) + elif self.fixed_scatter_proportion == False and self.fixed_survival_probability == True and self.partially_fixed_scatter_proportion == False: + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability(bin_centers, eff_array, *params) + elif self.partially_fixed_scatter_proportion == True and self.fixed_survival_probability == True: + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(bin_centers, eff_array, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_elevated_gaussian_reso(self, bin_centers, data_hist_freq, eff_array, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + fit_Hz = self.spectrum_func_elevated_gaussian_fixed_scatter_proportion(bin_centers, eff_array, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_composite_gaussian_reso(self, bin_centers, data_hist_freq, eff_array, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + fit_Hz = self.spectrum_func_composite_gaussian_fixed_scatter_proportion(bin_centers, eff_array, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_composite_gaussian_pedestal_factor_reso(self, bin_centers, data_hist_freq, eff_array, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + fit_Hz = self.spectrum_func_composite_gaussian_pedestal_factor_fixed_scatter_proportion(bin_centers, eff_array, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_composite_gaussian_scaled_reso(self, bin_centers, data_hist_freq, eff_array, params): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + # expectation + fit_Hz = self.spectrum_func_composite_gaussian_scaled_fixed_scatter_proportion(bin_centers, eff_array, *params) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_simulated_resolution_scaled(self, bin_centers, data_hist_freq, eff_array, params): + # expectation + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fixed_scatter_proportion(bin_centers, eff_array, *params) + nonzero_bins_index = np.where((data_hist_freq != 0) & (fit_Hz != 0)) + zero_bins_index = np.where((data_hist_freq == 0) | (fit_Hz == 0)) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def chi_2_Poisson_simulated_resolution_scaled_fit_recon_eff(self, bin_centers, data_hist_freq, eff_array, params): + # expectation + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_recon_eff(bin_centers, eff_array, *params) + nonzero_bins_index = np.where((data_hist_freq != 0) & (fit_Hz != 0)) + zero_bins_index = np.where((data_hist_freq == 0) | (fit_Hz == 0)) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def reduced_chi2_Pearson_Neyman_composite(self, data_hist_freq, fit_Hz, number_of_parameters): + nonzero_bins_index = np.where(data_hist_freq != 0)[0] + zero_bins_index = np.where(data_hist_freq == 0)[0] + fit_Hz_nonzero = fit_Hz[nonzero_bins_index] + data_Hz_nonzero = data_hist_freq[nonzero_bins_index] + fit_Hz_zero = fit_Hz[zero_bins_index] + data_Hz_zero = data_hist_freq[zero_bins_index] + chi2 = sum((fit_Hz_nonzero - data_Hz_nonzero)**2/data_Hz_nonzero) + sum((fit_Hz_nonzero - data_Hz_nonzero)**2/fit_Hz_nonzero) + reduced_chi2 = chi2/(len(data_hist_freq) - number_of_parameters) + return reduced_chi2 + + # following the expression in the paper Steve BAKER and Robert D. COUSINS, (1984) CLARIFICATION OF THE USE OF CHI-SQUARE AND LIKELIHOOD FUNCTIONS IN FITS TO HISTOGRAMS + def reduced_chi2_Poisson(self, data_hist_freq, fit_Hz, number_of_parameters): + nonzero_bins_index = np.where(data_hist_freq != 0) + zero_bins_index = np.where(data_hist_freq == 0) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + reduced_chi2 = chi2/(len(data_hist_freq) - number_of_parameters) + return reduced_chi2 + + #Gaussian instrumental resolution with multi gas scattering with scatter proportion floating, but no reconstruction eff and no detection eff + def make_spectrum(self, gauss_FWHM_eV, prob_parameter, scatter_proportion, emitted_peak='shake'): + gases = self.gases + max_scatters = self.max_scatters + current_path = self.path_to_scatter_spectra_file + # check_existence_of_scatter_files() + #filenames = list_files('scatter_spectra_files') + p = np.zeros(len(gases)) + p[0:-1] = scatter_proportion + p[-1] = 1 - sum(scatter_proportion) + scatter_spectra_file_path = os.path.join(current_path, 'scatter_spectra.npy') + scatter_spectra = np.load( + scatter_spectra_file_path, allow_pickle = True + ) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + emitted_peak = self.base_shape + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + current_working_spectrum = self.convolve_gaussian(current_working_spectrum, gauss_FWHM_eV) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += current_working_spectrum + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(len(self.gases))): + coefficient = coefficient/factorial(component)*p[i]**component + current_full_spectrum += coefficient*current_working_spectrum*prob_parameter**M + return current_full_spectrum + + # Produces a spectrum in real energy that can now be evaluated off of the SELA. + #def spectrum_func(x_keV,FWHM_G_eV,line_pos_keV,scatter_prob,amplitude): + def spectrum_func(self, bins_Hz, *p0): + B_field = p0[0] + FWHM_G_eV = p0[1] + amplitude = p0[2] + prob_parameter = p0[3] + N = len(self.gases) + scatter_proportion = p0[4:3+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + en_array_rev = ComplexLineShapeUtilities.flip_array(-1*en_loss_array) + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum(FWHM_G_eV, prob_parameter, scatter_proportion) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx],en_array_rev,full_spectrum) + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + return f + + # Call this function to fit a histogram of start frequencies with the model. + # Note that the data_hist_freq should be the StartFrequencies as given by katydid, + # which will be from ~0 MHZ to ~100 MHz. You must also pass this function the + # self.RF_ROI_MIN value from the metadata file of your data. + # You must also supply a guess for the self.B_field present for the run; + # 0.959 T is usually sufficient. + + def fit_data(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + bins_Hz_nonzero , data_hist_nonzero , data_hist_err = ComplexLineShapeUtilities.get_only_nonzero_bins(bins_Hz, data_hist_freq) + # Initial guesses for curve_fit + FWHM_guess = 5 + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + FWHM_eV_min = 1e-5 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) - ConversionFunctions.Energy(bins_Hz[-1], B_field_guess) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + N = len(self.gases) + p0_guess = [B_field_guess, FWHM_guess, amplitude_guess, prob_parameter_guess] + [scatter_proportion_guess]*(N-1) + p0_bounds = ([B_field_min, FWHM_eV_min, amplitude_min, prob_parameter_min] + [scatter_proportion_min]*(N-1), + [B_field_max, FWHM_eV_max, amplitude_max, prob_parameter_max] + [scatter_proportion_max]*(N-1) ) + # Actually do the fitting + params , cov = curve_fit(self.spectrum_func, bins_Hz_nonzero, data_hist_nonzero, sigma=data_hist_err, p0=p0_guess, bounds=p0_bounds) + # Name each of the resulting parameters and errors + ################### Generalize to N Gases ########################### + B_field_fit = params[0] + FWHM_eV_fit = params[1] + amplitude_fit = params[2] + prob_parameter_fit = params[3] + #starting at index 4, grabs every other entry. (which is how scattering probs are filled in for N gases) + scatter_proportion_fit = list(params[4:3+N])+[1- sum(params[4:3+N])] + total_counts_fit = amplitude_fit + + perr = np.sqrt(np.diag(cov)) + B_field_fit_err = perr[0] + FWHM_eV_fit_err = perr[1] + amplitude_fit_err = perr[2] + prob_parameter_fit_err = perr[3] + scatter_proportion_fit_err = list(perr[4:3+N])+[np.sqrt(sum(perr[4:3+N]**2))] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func(bins_Hz, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + + nonzero_bins_index = np.where(data_hist_freq != 0)[0] + zero_bins_index = np.where(data_hist_freq == 0)[0] + fit_Hz_nonzero = fit_Hz[nonzero_bins_index] + data_Hz_nonzero = data_hist_freq[nonzero_bins_index] + fit_Hz_zero = fit_Hz[zero_bins_index] + data_Hz_zero = data_hist_freq[zero_bins_index] + chi2 = sum((fit_Hz_nonzero - data_Hz_nonzero)**2/data_Hz_nonzero) + sum((fit_Hz_nonzero - data_Hz_nonzero)**2/fit_Hz_nonzero) + reduced_chi2 = chi2/(len(data_hist_freq)-4-len(self.gases)+1) + elapsed = time.time() - t + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Gaussian FWHM = '+str(round(FWHM_eV_fit,2))+' +/- '+str(round(FWHM_eV_fit_err,2))+' eV\n' + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Probability parameter \n= ' + "{:.2e}".format(prob_parameter_fit)\ + +' +/- ' + "{:.2e}".format(prob_parameter_fit_err)+'\n' + output_string += '-----------------\n' + for i in range(len(self.gases)): + output_string += '{} Scatter proportion \n= '.format(self.gases[i]) + "{:.8e}".format(scatter_proportion_fit[i])\ + +' +/- ' + "{:.2e}".format(scatter_proportion_fit_err[i])+'\n' + output_string += '-----------------\n' + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'cov': cov, + 'bins_keV': bins_keV, + 'fit': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'FWHM_eV_fit': FWHM_eV_fit, + 'FWHM_eV_fit_err': FWHM_eV_fit_err, + 'survival_prob_fit': prob_parameter_fit, + 'survival_prob_fit_err': prob_parameter_fit_err, + 'scatter_proportion_fit': scatter_proportion_fit, + 'scatter_proportion_fit_err': scatter_proportion_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # same as make_spectrum except that scatter_proportion is fixed + def make_spectrum_1(self, gauss_FWHM_eV, prob_parameter, emitted_peak='shake'): + gases = self.gases + current_path = self.path_to_scatter_spectra_file + # check_existence_of_scatter_files() + #filenames = list_files('scatter_spectra_files') + p = self.scatter_proportion + scatter_spectra_file_path = os.path.join(current_path, 'scatter_spectra.npy') + scatter_spectra = np.load( + scatter_spectra_file_path, allow_pickle = True + ) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + emitted_peak = self.base_shape + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + current_working_spectrum = self.convolve_gaussian(current_working_spectrum, gauss_FWHM_eV) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += current_working_spectrum + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(len(self.gases))): + coefficient = coefficient/factorial(component)*p[i]**component + current_full_spectrum += coefficient*current_working_spectrum*prob_parameter**M + return current_full_spectrum + + def spectrum_func_1(self, bins_Hz, *p0): + B_field = p0[0] + FWHM_G_eV = p0[1] + amplitude = p0[2] + prob_parameter = p0[3] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + en_array_rev = ComplexLineShapeUtilities.flip_array(-1*en_loss_array) + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_1(FWHM_G_eV, prob_parameter,) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx],en_array_rev,full_spectrum) + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + return f + + def fit_data_1(self, freq_bins, data_hist_freq): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + bins_Hz_nonzero , data_hist_nonzero , data_hist_err = ComplexLineShapeUtilities.get_only_nonzero_bins(bins_Hz, data_hist_freq) + # Initial guesses for curve_fit + FWHM_eV_guess = 5 + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + prob_parameter_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + FWHM_eV_min = 1e-5 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) - ConversionFunctions.Energy(bins_Hz[-1], B_field_guess) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + + # p0_guess = [B_field_guess, FWHM_guess, amplitude_guess, prob_parameter_guess] + # p0_bounds = ([B_field_min, FWHM_eV_min, amplitude_min, prob_parameter_min], + # [B_field_max, FWHM_eV_max, amplitude_max, prob_parameter_max]) + # Actually do the fitting + # params , cov = curve_fit(self.spectrum_func_1, bins_Hz_nonzero, data_hist_nonzero, sigma=data_hist_err, p0=p0_guess, bounds=p0_bounds) + p0_guess = [B_field_guess, FWHM_eV_guess, amplitude_guess, prob_parameter_guess] + p0_bounds = [(B_field_min,B_field_max), (FWHM_eV_min, FWHM_eV_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi2_Poisson(bins_Hz, data_hist_freq, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + # Name each of the resulting parameters and errors + ################### Generalize to N Gases ########################### + B_field_fit = params[0] + FWHM_eV_fit = params[1] + amplitude_fit = params[2] + prob_parameter_fit = params[3] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + FWHM_eV_fit_err = perr[1] + amplitude_fit_err = perr[2] + prob_parameter_fit_err = perr[3] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_1(bins_Hz, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + + nonzero_bins_index = np.where(data_hist_freq != 0)[0] + zero_bins_index = np.where(data_hist_freq == 0)[0] + fit_Hz_nonzero = fit_Hz[nonzero_bins_index] + data_Hz_nonzero = data_hist_freq[nonzero_bins_index] + fit_Hz_zero = fit_Hz[zero_bins_index] + data_Hz_zero = data_hist_freq[zero_bins_index] + reduced_chi2 = self.reduced_chi2_Poisson(data_hist_freq, fit_Hz, number_of_parameters = 4) + elapsed = time.time() - t + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Gaussian FWHM = '+str(round(FWHM_eV_fit,2))+' +/- '+str(round(FWHM_eV_fit_err,2))+' eV\n' + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Probability parameter \n= ' + "{:.2e}".format(prob_parameter_fit)\ + +' +/- ' + "{:.2e}".format(prob_parameter_fit_err)+'\n' + output_string += '-----------------\n' + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'FWHM_eV_fit': FWHM_eV_fit, + 'FWHM_eV_fit_err': FWHM_eV_fit_err, + 'survival_prob_fit': prob_parameter_fit, + 'survival_prob_fit_err': prob_parameter_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # using simulated resolution, with multi gas scattering, reconstruction eff, without detection eff, has been used in fake data generator. However, without using detection eff is the right option for tritium fake data generation. + def make_spectrum_ftc(self, survival_prob, emitted_peak='shake'): + gases = self.gases + current_path = self.path_to_scatter_spectra_file + # check_existence_of_scatter_files() + #filenames = list_files('scatter_spectra_files') + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(current_path, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + emitted_peak = self.base_shape + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + if self.use_combined_four_trap_inst_reso: + current_working_spectrum = self.convolve_ins_resolution_combining_four_trap(current_working_spectrum, self.trap_weights) + else: + current_working_spectrum = self.convolve_ins_resolution(current_working_spectrum) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += current_working_spectrum + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(len(self.gases))): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_ftc(self, bins_Hz, *p0): + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0], np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_ftc(survival_prob) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + return f + + def fit_data_ftc(self, freq_bins, data_hist_freq): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + bins_Hz_nonzero , data_hist_nonzero , data_hist_err = ComplexLineShapeUtilities.get_only_nonzero_bins(bins_Hz, data_hist_freq) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + prob_parameter_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi2_Poisson(bins_Hz, data_hist_freq, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + # Name each of the resulting parameters and errors + ################### Generalize to N Gases ########################### + B_field_fit = params[0] + amplitude_fit = params[1] + survival_prob_fit = params[2] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_ftc(bins_Hz, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + + reduced_chi2 = self.reduced_chi2_Poisson(data_hist_freq, fit_Hz, number_of_parameters = 3) + elapsed = time.time() - t + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability \n= ' + "{:.8e}".format(survival_prob_fit)\ + +' +/- ' + "{:.6e}".format(survival_prob_fit_err)+'\n' + output_string += '-----------------\n' + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + #simulated resolution with scatter_proportion floating, without reconstruction eff curve, without detection eff curve + def make_spectrum_ftc_2(self, prob_parameter, scatter_proportion, emitted_peak='shake'): + gases = self.gases + current_path = self.path_to_scatter_spectra_file + # check_existence_of_scatter_files() + #filenames = list_files('scatter_spectra_files') + p = np.zeros(len(gases)) + p[0:-1] = scatter_proportion + p[-1] = 1 - sum(scatter_proportion) + scatter_spectra_file_path = os.path.join(current_path, 'scatter_spectra.npy') + scatter_spectra = np.load( + scatter_spectra_file_path, allow_pickle = True + ) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + emitted_peak = self.base_shape + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + + if self.use_combined_four_trap_inst_reso: + current_working_spectrum = self.convolve_ins_resolution_combining_four_trap(current_working_spectrum, self.trap_weights) + else: + current_working_spectrum = self.convolve_ins_resolution(current_working_spectrum) + + zeroth_order_peak = current_working_spectrum + current_full_spectrum += current_working_spectrum + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(len(self.gases))): + coefficient = coefficient/factorial(component)*p[i]**component + current_full_spectrum += coefficient*current_working_spectrum*prob_parameter**M + return current_full_spectrum + + def spectrum_func_ftc_2(self, bins_Hz, *p0): + B_field = p0[0] + amplitude = p0[1] + prob_parameter = p0[2] + N = len(self.gases) + scatter_proportion = p0[3:2+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + en_array_rev = ComplexLineShapeUtilities.flip_array(-1*en_loss_array) + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_ftc_2(prob_parameter, scatter_proportion) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx],en_array_rev,full_spectrum) + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + return f + + def fit_data_ftc_2(self, freq_bins, data_hist_freq): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + bins_Hz_nonzero , data_hist_nonzero , data_hist_err = ComplexLineShapeUtilities.get_only_nonzero_bins(bins_Hz, data_hist_freq) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess] + (N-1)*[scatter_proportion_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max)] + (N-1)*[(scatter_proportion_min, scatter_proportion_max)] + logger.info(p0_guess) + logger.info(p0_bounds) + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi2_Poisson(bins_Hz, data_hist_freq, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + # Name each of the resulting parameters and errors + ################### Generalize to N Gases ########################### + B_field_fit = params[0] + amplitude_fit = params[1] + prob_parameter_fit = params[2] + scatter_proportion_fit = list(params[3:2+N])+[1- sum(params[3:2+N])] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + prob_parameter_fit_err = perr[2] + scatter_proportion_fit_err = list(perr[3:2+N])+[np.sqrt(sum(perr[3:2+N]**2))] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_ftc_2(bins_Hz, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + + reduced_chi2 = self.reduced_chi2_Poisson(data_hist_freq, fit_Hz, number_of_parameters = 4) + elapsed = time.time() - t + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Probability parameter \n= ' + "{:.2e}".format(prob_parameter_fit)+' +/- ' + "{:.2e}".format(prob_parameter_fit_err)+'\n' + output_string += '-----------------\n' + output_string += '' + for i in range(len(self.gases)): + output_string += '{} Scatter proportion \n= '.format(self.gases[i]) + "{:.8e}".format(scatter_proportion_fit[i])\ + +' +/- ' + "{:.2e}".format(scatter_proportion_fit_err[i])+'\n' + output_string += '-----------------\n' + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': prob_parameter_fit, + 'survival_prob_fit_err': prob_parameter_fit_err, + 'scatter_proportion_fit': scatter_proportion_fit, + 'scatter_proportion_fit_err': scatter_proportion_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + def make_spectrum_smeared_triangle(self, prob_parameter, center, scale1, scale2, exponent, sigma, emitted_peak='shake'): + current_path = self.get_current_path() + # check_existence_of_scatter_files() + #filenames = list_files('scatter_spectra_files') + p = np.zeros(len(self.gases)) + p = self.scatter_proportion + scatter_spectra = np.load('scatter_spectra_file/scatter_spectra.npy', allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_smeared_triangle(current_working_spectrum, center, scale1, scale2, exponent, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += current_working_spectrum + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(len(self.gases))): + coefficient = coefficient/factorial(component)*p[i]**component*prob_parameter**M + current_full_spectrum += coefficient*current_working_spectrum + + return current_full_spectrum + + def spectrum_func_smeared_triangle(self, bins_Hz, *p0): + + B_field = p0[0] + amplitude = p0[1] + prob_parameter = p0[2] + center = p0[3] + scale1 = p0[4] + scale2 = p0[5] + exponent = p0[6] + sigma = p0[7] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + en_array_rev = ComplexLineShapeUtilities.flip_array(-1*en_loss_array) + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_line()*1000 - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_smeared_triangle(prob_parameter, center, scale1, scale2, exponent, sigma) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + return f + + def fit_data_smeared_triangle(self, RF_ROI_MIN, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_files() + bins_Hz = freq_bins + RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + bins_Hz_nonzero , data_hist_nonzero , data_hist_err = self.get_only_nonzero_bins(bins_Hz, data_hist_freq) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + prob_parameter_guess = 0.5 + center_guess = 0 + scale_guess = 5 + exponent_guess = 1 + sigma_guess = 3 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + center_min = -5 + center_max = 5 + width_min = 0 + width_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) - ConversionFunctions.Energy(bins_Hz[-1], B_field_guess) + exponent_min = 0.5 + exponent_max = 2 + + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, center_guess, scale_guess, scale_guess, exponent_guess, sigma_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (center_min, center_max), (width_min, width_max), (width_min, width_max), (exponent_min, exponent_max), (width_min, width_max)] + #step_size = [1e-6, 5, 500, 0.1] + # Actually do the fitting + print(p0_guess) + print(p0_bounds) + m_binned = Minuit.from_array_func(lambda p: chi2_Poisson(bins_Hz, data_hist_freq, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + prob_parameter_fit = params[2] + center_fit = params[3] + scale1_fit = params[4] + scale2_fit = params[5] + exponent_fit = params[6] + sigma_fit = params[7] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + prob_parameter_fit_err = perr[2] + center_fit_err = perr[3] + scale1_fit_err = perr[4] + scale2_fit_err = perr[5] + exponent_fit_err = perr[6] + sigma_fit_err = perr[7] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = spectrum_func(bins_Hz,*params) + fit_keV = flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = flip_array(bins_keV) + reduced_chi2 = reduced_chi2_Poisson(data_hist_freq, fit_Hz, number_of_parameters = len(params)) + if print_params == True: + output_string = 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Probability parameter \n= ' + "{:.2e}".format(prob_parameter_fit) + ' +/- ' + "{:.2e}".format(prob_parameter_fit_err)+'\n' + output_string += '-----------------\n' + output_string += 'Center = {:.4e}'.format(center_fit) + ' +/- {:.4e}'.format(center_fit_err) + '\n' + output_string += '-----------------\n' + output_string += 'Scale1 = {:.4e}'.format(scale1_fit) + ' +/- {:.4e}'.format(scale1_fit_err) + '\n' + output_string += '-----------------\n' + output_string += 'Scale2 = {:.4e}'.format(scale2_fit) + ' +/- {:.4e}'.format(scale2_fit_err) + '\n' + output_string += '-----------------\n' + output_string += 'Exponent = {:.4e}'.format(exponent_fit) + ' +/- {:.4e}'.format(exponent_fit_err) + '\n' + output_string += '-----------------\n' + output_string += 'Sigma = {:.4e}'.format(sigma_fit) + ' +/- {:.4e}'.format(sigma_fit_err) + '\n' + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': prob_parameter_fit, + 'survival_prob_fit_err': prob_parameter_fit_err, + 'center_fit': scatter_proportion_fit, + 'center_fit_err': scatter_proportion_fit_err, + 'scale1_fit': scale1_fit, + 'scale1_fit_err': scale1_fit_err, + 'scale2_fit': scale2_fit, + 'scale2_fit_err': scale2_fit_err, + 'exponent_fit': exponent_fit, + 'exponent_fit_err': exponent_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + #fitting with commposite gaussian lorentzian resolution function and fixed scatter fraction + def make_spectrum_composite_gaussian_lorentzian_fixed_scatter_proportion(self, survival_prob, sigma, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_lorentzian(current_working_spectrum, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + sigma = p0[3] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_lorentzian_fixed_scatter_proportion(survival_prob, sigma) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_lorentzian_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, sigma_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (FWHM_eV_min, FWHM_eV_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_lorentzian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + sigma_fit = params[3] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + sigma_fit_err = perr[3] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'sigma = {:.2e}'.format(sigma_fit) + ' +/- {:.4e}\n'.format(sigma_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + + def make_spectrum_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(self, sigma, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + survival_prob = self.survival_prob + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_lorentzian(current_working_spectrum, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + sigma = p0[2] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(sigma) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, sigma_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (FWHM_eV_min, FWHM_eV_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_lorentzian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + sigma_fit = params[2] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + sigma_fit_err = perr[2] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_scatter_proportion_and_survival_prob(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'sigma = {:.2e}'.format(sigma_fit) + ' +/- {:.4e}\n'.format(sigma_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # fitting with composite gaussian lorentzian resolution function but scatter fraction floating + def make_spectrum_composite_gaussian_lorentzian_fixed_survival_probability(self, scatter_proportion, sigma, emitted_peak='shake'): + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + p = np.zeros(len(self.gases)) + p[0:-1] = scatter_proportion + p[-1] = 1 - sum(scatter_proportion) + survival_prob = self.survival_prob + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_lorentzian(current_working_spectrum, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + sigma = p0[2] + N = len(self.gases) + scatter_proportion = p0[3: 2+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_lorentzian_fixed_survival_probability(scatter_proportion, sigma) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_lorentzian_fixed_survival_probability(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, sigma_guess] + (N-1)*[scatter_proportion_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (FWHM_eV_min, FWHM_eV_max)] + (N-1)*[(scatter_proportion_min, scatter_proportion_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_lorentzian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + sigma_fit = params[2] + scatter_proportion_fit = list(params[3:2+N]) + [1- sum(params[3:2+N])] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + sigma_fit_err = perr[2] + scatter_proportion_fit_err = list(perr[3:2+N]) + [np.sqrt(sum(perr[3:2+N]**2))] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'sigma = {:.2e}'.format(sigma_fit) + ' +/- {:.4e}\n'.format(sigma_fit_err) + output_string += '-----------------\n' + for i in range(len(self.gases)): + output_string += '{} Scatter proportion \n= '.format(self.gases[i]) + "{:.6e}".format(scatter_proportion_fit[i])\ + +' +/- ' + "{:.2e}".format(scatter_proportion_fit_err[i])+'\n' + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + #fitting with composite gaussian lorentzian resolution function and the scatter fractions fixed for some gas species + def make_spectrum_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(self, scatter_proportion, sigma, emitted_peak='shake'): + p = scatter_proportion + tuple([1-sum(scatter_proportion)-sum(self.scatter_proportion_for_fixed_gases)]) + tuple(self.scatter_proportion_for_fixed_gases) + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + survival_prob = self.survival_prob + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_lorentzian(current_working_spectrum, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + sigma = p0[2] + N = len(self.free_gases) + scatter_proportion = p0[3: 2+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(scatter_proportion, sigma) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + N = len(self.free_gases) + p0_guess = [B_field_guess, amplitude_guess, sigma_guess] + (N-1)*[scatter_proportion_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (FWHM_eV_min, FWHM_eV_max)] + (N-1)*[(scatter_proportion_min, scatter_proportion_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_lorentzian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + sigma_fit = params[2] + scatter_proportion_fit = list(params[3:2+N]) + [1- sum(params[3:2+N]) - sum(self.scatter_proportion_for_fixed_gases)] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + sigma_fit_err = perr[2] + scatter_proportion_fit_err = list(perr[3:2+N]) + [np.sqrt(sum(perr[3:2+N]**2))] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_lorentzian_fixed_survival_probability_partially_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'sigma = {:.2e}'.format(sigma_fit) + ' +/- {:.4e}\n'.format(sigma_fit_err) + output_string += '-----------------\n' + for i in range(len(self.free_gases)): + output_string += '{} Scatter proportion \n= '.format(self.free_gases[i]) + "{:.6e}".format(scatter_proportion_fit[i])\ + +' +/- ' + "{:.2e}".format(scatter_proportion_fit_err[i])+'\n' + output_string += '-----------------\n' + for i in range(len(self.fixed_gases)): + output_string += '{} Scatter proportion (fixed) \n= '.format(self.fixed_gases[i]) + "{:.6e}\n".format(self.scatter_proportion_for_fixed_gases[i]) + output_string += '-----------------\n' + output_string += 'Survival probability (fixed) = {:2e}\n'.format(self.survival_prob) + output_string += '-----------------\n' + output_string += 'Gaussian + Lorentzian resolution:\n' + output_string += ' ratio of gamma to sigma (fixed) = {:2e}\n'.format(self.ratio_gamma_to_sigma) + output_string += ' gaussian proportion (fixed) = {:2e}\n'.format(self.gaussian_proportion) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # fitting with elevated gaussian resolution function + def make_spectrum_elevated_gaussian_fixed_scatter_proportion(self, survival_prob, sigma, elevation_factor, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_elevated_gaussian(current_working_spectrum, elevation_factor, sigma) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_elevated_gaussian_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + sigma = p0[3] + elevation_factor = p0[4] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_elevated_gaussian_fixed_scatter_proportion(survival_prob, sigma, elevation_factor) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_elevated_gaussian_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + elevation_factor_guess = 20 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + elevation_factor_min = 0 + elevation_factor_max = 500 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, sigma_guess, elevation_factor_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (FWHM_eV_min, FWHM_eV_max), (elevation_factor_min, elevation_factor_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_elevated_gaussian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + sigma_fit = params[3] + elevation_factor_fit = params[4] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + sigma_fit_err = perr[3] + elevation_factor_fit_err = perr[4] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_elevated_gaussian_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'sigma = {:.2e}'.format(sigma_fit) + ' +/- {:.4e}\n'.format(sigma_fit_err) + output_string += '-----------------\n' + output_string += 'elevation factor = {:.2e}'.format(elevation_factor_fit) + ' +/- {:.4e}\n'.format(elevation_factor_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'sigma_fit': sigma_fit, + 'sigma_fit_err': sigma_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # fitting with superposition of gaussians as resolution function + def make_spectrum_composite_gaussian_fixed_scatter_proportion(self, survival_prob, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian(current_working_spectrum) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_fixed_scatter_proportion(survival_prob) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + elevation_factor_guess = 20 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + elevation_factor_min = 0 + elevation_factor_max = 500 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + # fitting with composte gaussian resolution function with pedestal factor and fixed scatter fraction + def make_spectrum_composite_gaussian_pedestal_factor_fixed_scatter_proportion(self, survival_prob, pedestal_factor, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_pedestal_factor(current_working_spectrum, pedestal_factor) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_pedestal_factor_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + pedestal_factor = p0[3] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_pedestal_factor_fixed_scatter_proportion(survival_prob, pedestal_factor) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_pedestal_factor_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + pedestal_factor_guess = 1. + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + pedestal_factor_min = 1e-5 + pedestal_factor_max = 500 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, pedestal_factor_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (pedestal_factor_min, pedestal_factor_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_pedestal_factor_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + pedestal_factor_fit = params[3] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + pedestal_factor_fit_err = perr[3] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_pedestal_factor_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'pedestal factor = {:.8e}'.format(pedestal_factor_fit) + ' +/- {:.8e}\n'.format(pedestal_factor_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + def make_spectrum_composite_gaussian_scaled_fixed_scatter_proportion(self, survival_prob, scale_factor, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_composite_gaussian_scaled(current_working_spectrum, scale_factor) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_composite_gaussian_scaled_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + scale_factor = p0[3] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_composite_gaussian_scaled_fixed_scatter_proportion(survival_prob, scale_factor) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_composite_gaussian_scaled_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + scale_factor_guess = 1. + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 500 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, scale_factor_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (scale_factor_min, scale_factor_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_composite_gaussian_scaled_reso(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + scale_factor_fit = params[3] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + scale_factor_fit_err = perr[3] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_composite_gaussian_scaled_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'scale factor = {:.8e}'.format(scale_factor_fit) + ' +/- {:.8e}\n'.format(scale_factor_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + #This model incorporates reconstruction eff, detection eff, instrumental resolution width scaling, currently the best one for Kr spectrum fitting 20210126 + def make_spectrum_simulated_resolution_scaled_fixed_scatter_proportion(self, survival_prob, scale_factor, emitted_peak='shake'): + p = self.scatter_proportion + a = self.recon_eff_param_a + b = self.recon_eff_param_b + c = self.recon_eff_param_c + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_simulated_resolution_scaled(current_working_spectrum, scale_factor) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-b*M**c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-a*np.exp(-b*i**c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_simulated_resolution_scaled_fixed_scatter_proportion(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + scale_factor = p0[3] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_simulated_resolution_scaled_fixed_scatter_proportion(survival_prob, scale_factor) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_simulated_resolution_scaled_fixed_scatter_proportion(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + scale_factor_guess = 1. + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 500 + N = len(self.gases) + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, scale_factor_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (scale_factor_min, scale_factor_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_simulated_resolution_scaled(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + scale_factor_fit = params[3] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + scale_factor_fit_err = perr[3] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fixed_scatter_proportion(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'scale factor = {:.8e}'.format(scale_factor_fit) + ' +/- {:.8e}\n'.format(scale_factor_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + def make_spectrum_simulated_resolution_scaled_fit_recon_eff(self, survival_prob, scale_factor, recon_eff_a, recon_eff_b, recon_eff_c, emitted_peak='shake'): + p = self.scatter_proportion + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + current_working_spectrum = self.convolve_simulated_resolution_scaled(current_working_spectrum, scale_factor) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + relative_reconstruction_eff = np.exp(-1.*recon_eff_b*M**recon_eff_c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + for i in range(0, M): + coefficient = coefficient*(1-recon_eff_a*np.exp(-1.*recon_eff_b*i**recon_eff_c)) + current_full_spectrum += relative_reconstruction_eff*coefficient*current_working_spectrum*survival_prob**M + return current_full_spectrum + + def spectrum_func_simulated_resolution_scaled_fit_recon_eff(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_prob = p0[2] + scale_factor = p0[3] + recon_eff_a = p0[4] + recon_eff_b = p0[5] + recon_eff_c = p0[6] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_simulated_resolution_scaled_fit_recon_eff(survival_prob, scale_factor, recon_eff_a, recon_eff_b, recon_eff_c) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def fit_data_simulated_resolution_scaled_fit_recon_eff(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + FWHM_eV_guess = 5 + prob_parameter_guess = 0.5 + scatter_proportion_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + scale_factor_guess = 1. + recon_eff_parameter_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + prob_parameter_min = 1e-5 + prob_parameter_max = 1 + scatter_proportion_min = 1e-5 + scatter_proportion_max = 1 + mu_min = -FWHM_eV_max + mu_max = FWHM_eV_max + gaussian_portion_min = 1e-5 + gaussian_portion_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 500 + recon_eff_parameter_min = 1e-5 + recon_eff_parameter_max = 1 + p0_guess = [B_field_guess, amplitude_guess, prob_parameter_guess, scale_factor_guess, recon_eff_parameter_guess, recon_eff_parameter_guess, recon_eff_parameter_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (prob_parameter_min, prob_parameter_max), (scale_factor_min, scale_factor_max), (recon_eff_parameter_min, recon_eff_parameter_max), (recon_eff_parameter_min, recon_eff_parameter_max), (recon_eff_parameter_min, recon_eff_parameter_max)] + # Actually do the fitting + m_binned = Minuit.from_array_func(lambda p: self.chi_2_Poisson_simulated_resolution_scaled_fit_recon_eff(bins_Hz, data_hist_freq, eff_array, p), + start = p0_guess, + limit = p0_bounds, + throw_nan = True + ) + m_binned.migrad() + params = m_binned.np_values() + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_prob_fit = params[2] + scale_factor_fit = params[3] + recon_eff_a_fit = params[4] + recon_eff_b_fit = params[5] + recon_eff_c_fit = params[6] + total_counts_fit = amplitude_fit + + perr = m_binned.np_errors() + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_prob_fit_err = perr[2] + scale_factor_fit_err = perr[3] + recon_eff_a_fit_err = perr[4] + recon_eff_b_fit_err = perr[5] + recon_eff_c_fit_err = perr[6] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_recon_eff(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'Survival probability = {:.8e}'.format(survival_prob_fit) + ' +/- {:.8e}\n'.format(survival_prob_fit_err) + output_string += '-----------------\n' + output_string += 'scale factor = {:.8e}'.format(scale_factor_fit) + ' +/- {:.8e}\n'.format(scale_factor_fit_err) + output_string += '-----------------\n' + output_string += 'recon_eff_a = {:.8e}'.format(recon_eff_a_fit) + ' +/- {:.8e}\n'.format(recon_eff_a_fit_err) + output_string += '-----------------\n' + output_string += 'recon_eff_b = {:.8e}'.format(recon_eff_b_fit) + ' +/- {:.8e}\n'.format(recon_eff_b_fit_err) + output_string += '-----------------\n' + output_string += 'recon_eff_c = {:.8e}'.format(recon_eff_c_fit) + ' +/- {:.8e}\n'.format(recon_eff_c_fit_err) + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'survival_prob_fit': survival_prob_fit, + 'survival_prob_fit_err': survival_prob_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + return dictionary_of_fit_results + + def make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio(self, scale_factor, survival_probability, scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction, emitted_peak='shake'): + p = np.zeros(len(self.gases)) + p[0:-1] = scatter_fraction + p[-1] = 1 - sum(scatter_fraction) + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + current_working_spectrum = self.convolve_simulated_resolution_scaled(current_working_spectrum, scale_factor) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + scatter_peak_ratio = np.exp(-1.*scatter_peak_ratio_p*M**( -self.factor*scatter_peak_ratio_p + scatter_peak_ratio_q))#(-0.5179*scatter_peak_ratio_b + scatter_peak_ratio_c) -0.448 + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + current_full_spectrum += coefficient*current_working_spectrum*scatter_peak_ratio*survival_probability**M + return current_full_spectrum + + def spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + scale_factor = p0[2] + survival_probability = p0[3] + scatter_peak_ratio_p = p0[4] + scatter_peak_ratio_q = p0[5] + N = len(self.gases) + scatter_fraction = p0[6:5+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio(scale_factor, survival_probability, scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def chi_2_simulated_resolution_scaled_fit_scatter_peak_ratio(self, bin_centers, data_hist_freq, eff_array, params): + # expectation + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio(bin_centers, eff_array, *params) + nonzero_bins_index = np.where((data_hist_freq != 0) & (fit_Hz > 0)) + zero_bins_index = np.where((data_hist_freq == 0) | (fit_Hz <= 0)) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + if self.use_quad_trap_eff_interp == True: + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + else: + eff_array = np.ones(len(bins_Hz)) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq) + FWHM_eV_guess = 5 + survival_probability_guess = 0.5 + scatter_fraction_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + scale_factor_guess = 1 + scatter_peak_ratio_parameter_p_guess = 0.9 + scatter_peak_ratio_parameter_q_guess = 1.0 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + survival_probability_min = 1e-5 + survival_probability_max = 1 + scatter_fraction_min = 1e-5 + scatter_fraction_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 5 + scatter_peak_ratio_parameter_min = 1e-5 + scatter_peak_ratio_parameter_max = 5 + N = len(self.gases) + gas_scatter_fraction_parameter_str = [] + for i in range(N-1): + gas_scatter_fraction_parameter_str += [self.gases[i]+' scatter fraction'] + p0_guess = [B_field_guess, amplitude_guess, scale_factor_guess, survival_probability_guess, scatter_peak_ratio_parameter_p_guess, scatter_peak_ratio_parameter_q_guess]+ (N-1)*[scatter_fraction_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min, amplitude_max), (scale_factor_min, scale_factor_max), (survival_probability_min, survival_probability_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max)] + (N-1)*[(scatter_fraction_min, scatter_fraction_max)] + parameter_names = ['B field','amplitude','width scale factor', 'survival probability','scatter peak ratio param b', 'scatter peak ratio param c'] + gas_scatter_fraction_parameter_str + # Actually do the fitting + m_binned = Minuit(lambda p: self.chi_2_simulated_resolution_scaled_fit_scatter_peak_ratio(bins_Hz, data_hist_freq, eff_array, p), p0_guess, name = parameter_names) + m_binned.limits = p0_bounds + if len(self.fixed_parameter_names)>0: + for fixed_parameter_name, fixed_parameter_value in zip(self.fixed_parameter_names, self.fixed_parameter_values): + m_binned.fixed[fixed_parameter_name] = True + m_binned.values[fixed_parameter_name] = fixed_parameter_value + m_binned.errors[fixed_parameter_name] = 0 + m_binned.migrad() + m_binned.hesse() + params = m_binned.values[0:] + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + scale_factor_fit = params[2] + survival_probability_fit = params[3] + scatter_peak_ratio_p_fit = params[4] + scatter_peak_ratio_q_fit = params[5] + total_counts_fit = amplitude_fit + logger.info('\n'+str(m_binned.params)) + scatter_fraction_fit = params[6:5+N]+[1- sum(params[6:5+N])] + + perr = m_binned.errors[0:] + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + scale_factor_fit_err = perr[2] + survival_probability_fit_err = perr[3] + scatter_peak_ratio_p_fit_err = perr[4] + scatter_peak_ratio_q_fit_err = perr[5] + total_counts_fit_err = amplitude_fit_err + scatter_fraction_fit_err = perr[6:5+N]+[np.sqrt(sum(np.array(perr[6:5+N])**2))] + + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + correlation_matrix = m_binned.covariance.correlation() + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'width scaling factor = {:.8e}'.format(scale_factor_fit) + ' +/- {:.8e}\n'.format(scale_factor_fit_err) + output_string += '-----------------\n' + output_string += 'survival probability = {:.8e}'.format(survival_probability_fit) + ' +/- {:.8e}\n'.format(survival_probability_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_p = {:.8e}'.format(scatter_peak_ratio_p_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_p_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_q = {:.8e}'.format(scatter_peak_ratio_q_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_q_fit_err) + output_string += '-----------------\n' + for i in range(len(self.gases)): + output_string += '{} scatter fraction \n= '.format(self.gases[i]) + "{:.8e}".format(scatter_fraction_fit[i])\ + +' +/- ' + "{:.8e}".format(scatter_fraction_fit_err[i])+'\n' + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'scale_factor_fit': scale_factor_fit, + 'scale_factor_fit_err': scale_factor_fit_err, + 'scatter_peak_ratio_p_fit': scatter_peak_ratio_p_fit, + 'scatter_peak_ratio_p_fit_err': scatter_peak_ratio_p_fit_err, + 'scatter_peak_ratio_q_fit': scatter_peak_ratio_q_fit, + 'scatter_peak_ratio_q_fit_err': scatter_peak_ratio_q_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2, + 'correlation_matrix': np.array(correlation_matrix) + } + return dictionary_of_fit_results + + + def make_spectrum_gaussian_resolution_fit_scatter_peak_ratio(self, gauss_FWHM_eV, survival_probability, scatter_peak_ratio_p, scatter_peak_ratio_q, scatter_fraction, emitted_peak='shake'): + p = np.zeros(len(self.gases)) + p[0:-1] = scatter_fraction + p[-1] = 1 - sum(scatter_fraction) + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + current_working_spectrum = self.convolve_gaussian(current_working_spectrum, gauss_FWHM_eV) + zeroth_order_peak = current_working_spectrum + current_full_spectrum += zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + scatter_peak_ratio = np.exp(-1.*scatter_peak_ratio_p*M**( -self.factor*scatter_peak_ratio_p + scatter_peak_ratio_q))#np.exp(-1.*scatter_peak_ratio_b*M**scatter_peak_ratio_c) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + #print(combination) + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + current_full_spectrum += coefficient*current_working_spectrum*scatter_peak_ratio*survival_probability**M + return current_full_spectrum + + def spectrum_func_gaussian_resolution_fit_scatter_peak_ratio(self, bins_Hz, eff_array, *p0): + + B_field = p0[0] + amplitude = p0[1] + gauss_FWHM_eV = p0[2] + survival_probability = p0[3] + scatter_peak_ratio_b = p0[4] + scatter_peak_ratio_c = p0[5] + N = len(self.gases) + scatter_fraction = p0[6:5+N] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_gaussian_resolution_fit_scatter_peak_ratio(gauss_FWHM_eV, survival_probability, scatter_peak_ratio_b, scatter_peak_ratio_c, scatter_fraction) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def chi_2_gaussian_resolution_fit_scatter_peak_ratio(self, bin_centers, data_hist_freq, eff_array, params): + # expectation + fit_Hz = self.spectrum_func_gaussian_resolution_fit_scatter_peak_ratio(bin_centers, eff_array, *params) + nonzero_bins_index = np.where((data_hist_freq != 0) & (fit_Hz > 0)) + zero_bins_index = np.where((data_hist_freq == 0) | (fit_Hz <= 0)) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def fit_data_gaussian_resolution_fit_scatter_peak_ratio(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + if self.use_quad_trap_eff_interp == True: + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + else: + eff_array = np.ones(len(bins_Hz)) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq)/2 + gauss_FWHM_eV_guess = 1 + survival_probability_guess = 0.5 + scatter_fraction_guess = 0.5 + scale_factor_guess = 0.1 + scatter_peak_ratio_parameter_guess = 0.5 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + gauss_FWHM_eV_min = 1e-5 + gauss_FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess)-ConversionFunctions.Energy(bins_Hz[-1], B_field_guess) + survival_probability_min = 1e-5 + survival_probability_max = 1 + scatter_fraction_min = 1e-5 + scatter_fraction_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 5 + scatter_peak_ratio_parameter_min = 1e-5 + scatter_peak_ratio_parameter_max = 5 + N = len(self.gases) + gas_scatter_fraction_parameter_str = [] + for i in range(N-1): + gas_scatter_fraction_parameter_str += [self.gases[i]+' scatter fraction'] + p0_guess = [B_field_guess, amplitude_guess, gauss_FWHM_eV_guess, survival_probability_guess, scatter_peak_ratio_parameter_guess, scatter_peak_ratio_parameter_guess]+ (N-1)*[scatter_fraction_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min,amplitude_max), (gauss_FWHM_eV_min, gauss_FWHM_eV_max), (survival_probability_min, survival_probability_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max)] + (N-1)*[(scatter_fraction_min, scatter_fraction_max)] + parameter_names = ['B field','amplitude','gaussian FWHM eV', 'survival probability','scatter peak ratio param b', 'scatter peak ratio param c'] + gas_scatter_fraction_parameter_str + # Actually do the fitting + m_binned = Minuit(lambda p: self.chi_2_gaussian_resolution_fit_scatter_peak_ratio(bins_Hz, data_hist_freq, eff_array, p), p0_guess, name = parameter_names) + m_binned.limits = p0_bounds + if len(self.fixed_parameter_names)>0: + for fixed_parameter_name, fixed_parameter_value in zip(self.fixed_parameter_names, self.fixed_parameter_values): + m_binned.fixed[fixed_parameter_name] = True + m_binned.values[fixed_parameter_name] = fixed_parameter_value + m_binned.errors[fixed_parameter_name] = 0 + m_binned.migrad() + m_binned.hesse() + params = m_binned.values[0:] + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + gauss_FWHM_eV_fit = params[2] + survival_probability_fit = params[3] + scatter_peak_ratio_b_fit = params[4] + scatter_peak_ratio_c_fit = params[5] + total_counts_fit = amplitude_fit + logger.info('\n'+str(m_binned.params)) + scatter_fraction_fit = params[6:5+N]+[1- sum(params[6:5+N])] + + perr = m_binned.errors[0:] + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + gauss_FWHM_eV_fit_err = perr[2] + survival_probability_fit_err = perr[3] + scatter_peak_ratio_b_fit_err = perr[4] + scatter_peak_ratio_c_fit_err = perr[5] + total_counts_fit_err = amplitude_fit_err + scatter_fraction_fit_err = perr[6:5+N]+[np.sqrt(sum(np.array(perr[6:5+N])**2))] + + fit_Hz = self.spectrum_func_gaussian_resolution_fit_scatter_peak_ratio(bins_Hz, eff_array, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'gaussian FWHM = {:.8e}'.format(gauss_FWHM_eV_fit) + ' +/- {:.8e} eV\n'.format(gauss_FWHM_eV_fit_err) + output_string += '-----------------\n' + output_string += 'survival probability = {:.8e}'.format(survival_probability_fit) + ' +/- {:.8e}\n'.format(survival_probability_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_b = {:.8e}'.format(scatter_peak_ratio_b_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_b_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_c = {:.8e}'.format(scatter_peak_ratio_c_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_c_fit_err) + output_string += '-----------------\n' + for i in range(len(self.gases)): + output_string += '{} scatter fraction \n= '.format(self.gases[i]) + "{:.8e}".format(scatter_fraction_fit[i])\ + +' +/- ' + "{:.8e}".format(scatter_fraction_fit_err[i])+'\n' + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'gauss_FWHM_eV_fit': gauss_FWHM_eV_fit, + 'gauss_FWHM_eV_fit_err': gauss_FWHM_eV_fit_err, + 'scatter_peak_ratio_b_fit': scatter_peak_ratio_b_fit, + 'scatter_peak_ratio_b_fit_err': scatter_peak_ratio_b_fit_err, + 'scatter_peak_ratio_c_fit': scatter_peak_ratio_c_fit, + 'scatter_peak_ratio_c_fit_err': scatter_peak_ratio_c_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2 + } + + return dictionary_of_fit_results + + def generate_scatter_peaks(self): + + p = np.zeros(len(self.gases)) + scatter_fraction = self.scatter_fractions_for_gases + p[0:-1] = scatter_fraction + p[-1] = 1 - sum(scatter_fraction) + + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + + scatter_peaks = np.zeros((self.max_scatters+1, len(en_array))) + emitted_peak = self.base_shape + if emitted_peak == 'lorentzian': + current_working_spectrum = self.std_lorenztian_17keV() + elif emitted_peak == 'shake': + current_working_spectrum = self.shakeSpectrumClassInstance.shake_spectrum() + elif emitted_peak == 'dirac': + current_working_spectrum = self.std_dirac() + + scale_factor = 1 + current_working_spectrum = self.convolve_simulated_resolution_scaled(current_working_spectrum, scale_factor) + zeroth_order_peak = current_working_spectrum + scatter_peaks[0] = zeroth_order_peak + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + current_scatter_peak_spectrum = np.zeros(len(en_array)) + gas_scatter_combinations = np.array([np.array(i) for i in product(range(M+1), repeat=N) if sum(i)==M]) + for combination in gas_scatter_combinations: + entry_str = '' + for component, gas_type in zip(combination, self.gases): + entry_str += gas_type + entry_str += str(component).zfill(2) + current_working_spectrum = scatter_spectra.item()[entry_str] + current_working_spectrum = self.normalize(signal.convolve(zeroth_order_peak, current_working_spectrum, mode='same')) + coefficient = factorial(sum(combination)) + for component, i in zip(combination, range(N)): + coefficient = coefficient/factorial(component)*p[i]**component + current_scatter_peak_spectrum += coefficient*current_working_spectrum + scatter_peaks[M] = current_scatter_peak_spectrum + return scatter_peaks + + def make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(self, scatter_peaks, survival_probability, scatter_peak_ratio_p, scatter_peak_ratio_q): + scatter_spectra_file_path = os.path.join(self.path_to_scatter_spectra_file, 'scatter_spectra.npy') + scatter_spectra = np.load(scatter_spectra_file_path, allow_pickle = True) + en_array = self.std_eV_array() + current_full_spectrum = np.zeros(len(en_array)) + current_full_spectrum += scatter_peaks[0] + N = len(self.gases) + for M in range(1, self.max_scatters + 1): + scatter_peak_ratio = np.exp(-1.*scatter_peak_ratio_p*M**( - self.factor*scatter_peak_ratio_p + scatter_peak_ratio_q))#(-0.5179*scatter_peak_ratio_b + scatter_peak_ratio_c) -0.448 -0.4934 + current_full_spectrum += scatter_peaks[M]*scatter_peak_ratio*survival_probability**M + return current_full_spectrum + + def spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(self, bins_Hz, eff_array, scatter_peaks, *p0): + + B_field = p0[0] + amplitude = p0[1] + survival_probability = p0[2] + scatter_peak_ratio_p = p0[3] + scatter_peak_ratio_q = p0[4] + + x_eV = ConversionFunctions.Energy(bins_Hz, B_field) + en_loss_array = self.std_eV_array() + en_loss_array_min = en_loss_array[0] + en_loss_array_max = en_loss_array[len(en_loss_array)-1] + f = np.zeros(len(x_eV)) + f_intermediate = np.zeros(len(x_eV)) + + x_eV_minus_line = Constants.kr_k_line_e() - x_eV + zero_idx = np.r_[np.where(x_eV_minus_line< en_loss_array_min)[0],np.where(x_eV_minus_line>en_loss_array_max)[0]] + nonzero_idx = [i for i in range(len(x_eV)) if i not in zero_idx] + + full_spectrum = self.make_spectrum_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(scatter_peaks, survival_probability, scatter_peak_ratio_p, scatter_peak_ratio_q) + f_intermediate[nonzero_idx] = np.interp(x_eV_minus_line[nonzero_idx], en_loss_array, full_spectrum) + f_intermediate = f_intermediate*eff_array + f[nonzero_idx] += amplitude*f_intermediate[nonzero_idx]/np.sum(f_intermediate[nonzero_idx]) + + return f + + def chi_2_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(self, bin_centers, data_hist_freq, eff_array, scatter_peaks, params): + # expectation + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(bin_centers, eff_array, scatter_peaks, *params) + nonzero_bins_index = np.where((data_hist_freq != 0) & (fit_Hz > 0)) + zero_bins_index = np.where((data_hist_freq == 0) | (fit_Hz <= 0)) + chi2 = 2*((fit_Hz - data_hist_freq + data_hist_freq*np.log(data_hist_freq/fit_Hz))[nonzero_bins_index]).sum() + chi2 += 2*(fit_Hz - data_hist_freq)[zero_bins_index].sum() + return chi2 + + def fit_data_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(self, freq_bins, data_hist_freq, print_params=True): + t = time.time() + self.check_existence_of_scatter_file() + bins_Hz = freq_bins + self.RF_ROI_MIN + bins_Hz = 0.5*(bins_Hz[1:] + bins_Hz[:-1]) + if self.use_quad_trap_eff_interp == True: + quad_trap_interp = np.load(self.path_to_quad_trap_eff_interp, allow_pickle = True) + quad_trap_count_rate_interp = quad_trap_interp.item()['count_rate_interp'] + eff_array = quad_trap_count_rate_interp(bins_Hz) + else: + eff_array = np.ones(len(bins_Hz)) + # Initial guesses for curve_fit + B_field_guess = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[np.argmax(data_hist_freq)]) + amplitude_guess = np.sum(data_hist_freq) + FWHM_eV_guess = 5 + survival_probability_guess = 0.5 + scatter_fraction_guess = 0.5 + sigma_guess = 5 + gamma_guess = 3 + gaussian_portion_guess = 0.5 + scale_factor_guess = 1 + scatter_peak_ratio_parameter_guess = 0.7 + # Bounds for curve_fit + B_field_min = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[0]) + B_field_max = ComplexLineShapeUtilities.central_frequency_to_B_field(bins_Hz[-1]) + amplitude_min = 1e-5 + amplitude_max = np.sum(data_hist_freq)*3 + FWHM_eV_min = 0 + FWHM_eV_max = ConversionFunctions.Energy(bins_Hz[0], B_field_guess) + survival_probability_min = 1e-5 + survival_probability_max = 1 + scatter_fraction_min = 1e-5 + scatter_fraction_max = 1 + scale_factor_min = 1e-5 + scale_factor_max = 5 + scatter_peak_ratio_parameter_min = 1e-5 + scatter_peak_ratio_parameter_max = 5 + N = len(self.gases) + gas_scatter_fraction_parameter_str = [] + for i in range(N-1): + gas_scatter_fraction_parameter_str += [self.gases[i]+' scatter fraction'] + p0_guess = [B_field_guess, amplitude_guess, survival_probability_guess, scatter_peak_ratio_parameter_guess, scatter_peak_ratio_parameter_guess] + p0_bounds = [(B_field_min,B_field_max), (amplitude_min, amplitude_max), (survival_probability_min, survival_probability_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max), (scatter_peak_ratio_parameter_min, scatter_peak_ratio_parameter_max)] + parameter_names = ['B field','amplitude', 'survival probability','scatter peak ratio param p', 'scatter peak ratio param q'] + + scatter_peaks = self.generate_scatter_peaks() + # Actually do the fitting + m_binned = Minuit(lambda p: self.chi_2_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(bins_Hz, data_hist_freq, eff_array, scatter_peaks, p), p0_guess, name = parameter_names) + m_binned.limits = p0_bounds + if len(self.fixed_parameter_names)>0: + for fixed_parameter_name, fixed_parameter_value in zip(self.fixed_parameter_names, self.fixed_parameter_values): + m_binned.fixed[fixed_parameter_name] = True + m_binned.values[fixed_parameter_name] = fixed_parameter_value + m_binned.errors[fixed_parameter_name] = 0 + m_binned.migrad() + m_binned.hesse() + params = m_binned.values[0:] + B_field_fit = params[0] + #starting at index 2, grabs every other entry. (which is how scattering probs are filled in for N gases) + amplitude_fit = params[1] + survival_probability_fit = params[2] + scatter_peak_ratio_p_fit = params[3] + scatter_peak_ratio_q_fit = params[4] + total_counts_fit = amplitude_fit + logger.info('\n'+str(m_binned.params)) + + perr = m_binned.errors[0:] + B_field_fit_err = perr[0] + amplitude_fit_err = perr[1] + survival_probability_fit_err = perr[2] + scatter_peak_ratio_p_fit_err = perr[3] + scatter_peak_ratio_q_fit_err = perr[4] + total_counts_fit_err = amplitude_fit_err + + fit_Hz = self.spectrum_func_simulated_resolution_scaled_fit_scatter_peak_ratio_with_fixed_gas_composition_and_width_scale_factor(bins_Hz, eff_array, scatter_peaks, *params) + fit_keV = ComplexLineShapeUtilities.flip_array(fit_Hz) + bins_keV = ConversionFunctions.Energy(bins_Hz, B_field_fit)/1000 + bins_keV = ComplexLineShapeUtilities.flip_array(bins_keV) + reduced_chi2 = m_binned.fval/(len(fit_Hz)-m_binned.nfit) + correlation_matrix = m_binned.covariance.correlation() + + if print_params == True: + output_string = '\n' + output_string += 'Reduced chi^2 = {:.2e}\n'.format(reduced_chi2) + output_string += '-----------------\n' + output_string += 'B field = {:.8e}'.format(B_field_fit)+' +/- '+ '{:.4e} T\n'.format(B_field_fit_err) + output_string += '-----------------\n' + output_string += 'Amplitude = {}'.format(round(amplitude_fit,2))+' +/- {}'.format(round(amplitude_fit_err,2)) + '\n' + output_string += '-----------------\n' + output_string += 'survival probability = {:.8e}'.format(survival_probability_fit) + ' +/- {:.8e}\n'.format(survival_probability_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_p = {:.8e}'.format(scatter_peak_ratio_p_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_p_fit_err) + output_string += '-----------------\n' + output_string += 'scatter_peak_ratio_q = {:.8e}'.format(scatter_peak_ratio_q_fit) + ' +/- {:.8e}\n'.format(scatter_peak_ratio_q_fit_err) + output_string += '-----------------\n' + scatter_fraction = np.zeros(len(self.gases)) + scatter_fraction[0:-1] = self.scatter_fractions_for_gases + scatter_fraction[-1] = 1 - sum(scatter_fraction) + for i in range(len(self.gases)): + output_string += '{} scatter fraction = '.format(self.gases[i]) + "{:.8e}".format(scatter_fraction[i])+'\n' + output_string += '-----------------\n' + elapsed = time.time() - t + output_string += 'Fit completed in '+str(round(elapsed,2))+'s'+'\n' + dictionary_of_fit_results = { + 'output_string': output_string, + 'perr': perr, + 'bins_keV': bins_keV, + 'fit_keV': fit_keV, + 'bins_Hz': bins_Hz, + 'fit_Hz': fit_Hz, + 'B_field_fit': B_field_fit, + 'B_field_fit_err': B_field_fit_err, + 'scatter_peak_ratio_p_fit': scatter_peak_ratio_p_fit, + 'scatter_peak_ratio_p_fit_err': scatter_peak_ratio_p_fit_err, + 'scatter_peak_ratio_q_fit': scatter_peak_ratio_q_fit, + 'scatter_peak_ratio_q_fit_err': scatter_peak_ratio_q_fit_err, + 'amplitude_fit': amplitude_fit, + 'amplitude_fit_err': amplitude_fit_err, + 'data_hist_freq': data_hist_freq, + 'reduced_chi2': reduced_chi2, + 'correlation_matrix': np.array(correlation_matrix) + } + return dictionary_of_fit_results \ No newline at end of file diff --git a/test_analysis/Complex_line_shape_fitter.py b/test_analysis/Complex_line_shape_fitter.py index 352288ef..43309b7d 100644 --- a/test_analysis/Complex_line_shape_fitter.py +++ b/test_analysis/Complex_line_shape_fitter.py @@ -7,6 +7,8 @@ import numpy as np import unittest import matplotlib.pyplot as plt +import ROOT as r +import os from morpho.utilities import morphologging, parser logger = morphologging.getLogger(__name__) @@ -15,45 +17,78 @@ class ComplexLineShapeTests(unittest.TestCase): def test_complex_lineshape(self): from mermithid.processors.IO import IOCicadaProcessor - from mermithid.processors.misc.KrComplexLineShape import KrComplexLineShape + from mermithid.processors.misc.MultiGasComplexLineShape import MultiGasComplexLineShape reader_config = { "action": "read", - "filename": "/host/ShallowTrap8603-8669.root", + "filename": "/host/october_2019_kr_calibration_channel_b_merged.root", "object_type": "TMultiTrackEventData", "object_name": "multiTrackEvents:Event", "use_katydid": False, "variables": ['StartTimeInAcq','StartFrequency'] } + complexLineShape_config = { - 'bins_choice': np.linspace(0,90e6,1000), - 'gases': ["H2","Kr"], + 'bins_choice': np.linspace(0e6, 100e6, 1000), + 'gases': ["H2", "He"], # "Ar", "Kr" # "Kr" for fss + 'fix_gas_composition': True, + 'fix_width_scale_factor': True, + 'factor': 0.4626, + 'scatter_fractions_for_gases': [0.894], 'max_scatters': 20, - 'fix_scatter_proportion': True, - # When fix_scatter_proportion is True, set the scatter proportion for gas1 below - 'gas1_scatter_proportion': 0.8, + 'fixed_scatter_proportion': True, + # When fixed_scatter_proportion is True, set the scatter proportion for the gases below + 'gas_scatter_proportion': [0.8, 0.2],#0.827, 0.076, 0.068, 0.028 # 0.75, 0.25 + 'partially_fixed_scatter_proportion': False, + 'free_gases': ["H2", "He"], + 'fixed_gases': ["Ar", "Kr"], + 'scatter_proportion_for_fixed_gases': [0.018, 0.039], + 'use_radiation_loss': True, + 'sample_ins_res_errors': False, + 'fixed_survival_probability': False, + # When option fixed_survival_probability is True, assign the survival probability below + 'survival_prob': 15/16., # assuming total cross section for elastic scattering is 1/10 of inelastic scattering + # configure the resolution functions: simulated_resolution, gaussian_resolution, gaussian_lorentzian_composite_resolution, elevated_gaussian, composite_gaussian, composite_gaussian_pedestal_factor, composite_gaussian_scaled, simulated_resolution_scaled, 'simulated_resolution_scaled_fit_scatter_peak_ratio', 'gaussian_resolution_fit_scatter_peak_ratio' + 'resolution_function': 'simulated_resolution_scaled_fit_scatter_peak_ratio', + # specific choice of parameters in the gaussian lorentzian composite resolution function + 'recon_eff_param_a': 0.005569990343215976, + 'recon_eff_param_b': 0.351, + 'recon_eff_param_c': 0.546, + 'ratio_gamma_to_sigma': 0.8, + 'gaussian_proportion': 1., + # if the resolution function is composite gaussian + 'sigma_array': [5.01, 13.33, 15.40, 11.85], + 'A_array': [0.076, 0.341, 0.381, 0.203], + #parameter for simulated resolution scaled resolution + 'fit_recon_eff': False, + #parameters for simulated resolution scaled with scatter peak ratio fitted + #choose the parameters you want to fix from ['B field','amplitude', 'width scale factor', 'survival probability','scatter peak ratio param b', 'scatter peak ratio param c'] plus the gas scatter fractions as ['H2 scatter fraction'], + 'fixed_parameter_names': ['survival probability'], #, 'width scale factor', 'H2 scatter fraction', 'He scatter fraction', 'Ar scatter fraction' + 'fixed_parameter_values': [1.0], #[1.0, 1.0, 0.886, 0.02, 0.06] # This is an important parameter which determines how finely resolved # the scatter calculations are. 10000 seems to produce a stable fit, with minimal slowdown - 'num_points_in_std_array': 10000, - 'RF_ROI_MIN': 25850000000.0, - 'B_field': 0.957810722501, + 'num_points_in_std_array': 4000, + 'RF_ROI_MIN': 25859375000.0, #24.5e9 + 1.40812680e+09 - 50e6, #25850000000.0 # shake_spectrum_parameters.json and oscillator strength data can be found at https://github.com/project8/scripts/tree/master/yuhao/line_shape_fitting/data 'shake_spectrum_parameters_json_path': '../mermithid/misc/shake_spectrum_parameters.json', - 'path_to_osc_strengths_files': '/host/' + 'path_to_osc_strengths_files': '/host/', + 'path_to_scatter_spectra_file': '/host/', + 'path_to_ins_resolution_data_txt': '/host/March_FTC_resolution/all_res_cf15.300.txt' + } b = IOCicadaProcessor("reader") - complexLineShape = KrComplexLineShape("complexLineShape") - b.Configure(reader_config) - complexLineShape.Configure(complexLineShape_config) - b.Run() data = b.data logger.info("Data extracted = {}".format(data.keys())) for key in data.keys(): logger.info("{} -> size = {}".format(key,len(data[key]))) + + complexLineShape = MultiGasComplexLineShape("complexLineShape") + + complexLineShape.Configure(complexLineShape_config) complexLineShape.data = data @@ -63,7 +98,7 @@ def test_complex_lineshape(self): logger.info(results['output_string']) # plot fit with shake spectrum - plt.rcParams.update({'font.size': 20}) + plt.rcParams.update({'font.size': 15}) plt.figure(figsize=(15,9)) plt.step( results['bins_Hz']/1e9, results['data_hist_freq'], @@ -72,8 +107,12 @@ def test_complex_lineshape(self): plt.plot(results['bins_Hz']/1e9, results['fit_Hz'], label = results['output_string'], alpha = 0.7) plt.legend(loc = 'upper left', fontsize = 12) plt.xlabel('frequency GHz') - plt.title('fit with shake spectrum 2 gas scattering') - plt.savefig('fit_shake_2_gas_0.png') + plot_title = 'fit ftc march with gases: {},\n resolution function: {},\n file for simulated resolution data: {}'.format(complexLineShape_config['gases'], complexLineShape_config['resolution_function'], os.path.basename(complexLineShape_config['path_to_ins_resolution_data_txt'])) + if complexLineShape_config['resolution_function'] == 'composite_gaussian_scaled': + plot_title = 'fit ftc march with gases: {},\n scatter proportion: {},\n resolution function: {},\n sigma_array: {},\n A_array: {},\n'.format(complexLineShape_config['gases'], complexLineShape_config['gas_scatter_proportion'], complexLineShape_config['resolution_function'], complexLineShape_config['sigma_array'], complexLineShape_config['A_array']) + plt.title(plot_title) + plt.tight_layout() + plt.savefig('/host/plots/fit_october_ftc_simulated_resolution.png') if __name__ == '__main__': diff --git a/test_analysis/fake_data_stan_analysis.py b/test_analysis/fake_data_stan_analysis.py index 00ebdce0..a29489c5 100644 --- a/test_analysis/fake_data_stan_analysis.py +++ b/test_analysis/fake_data_stan_analysis.py @@ -112,7 +112,8 @@ def GenerateFakeData(inputs_dict): """ specGen_config = { "apply_efficiency": True, - "efficiency_path": "../phase2_detection_efficiency_curve/combined_energy_corrected_count_rates/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + "efficiency_path": "../tests/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + 'path_to_ins_resolution_data_txt': '/host/ins_resolution_all4.txt' "detailed_or_simplified_lineshape": "detailed", "return_frequency": True, "Q": inputs_dict["Q"], @@ -154,7 +155,7 @@ def BinAndSaveData(tritium_data, nbins, root_file="./results/tritium_analysis.ro "energy_or_frequency": 'frequency', "variables": "F", "title": "corrected_spectrum", - "efficiency_filepath": "../phase2_detection_efficiency_curve/combined_energy_corrected_count_rates/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + "efficiency_filepath": "../tests/combined_energy_corrected_eff_at_quad_trap_frequencies.json", 'bins': np.linspace(tritium_data['minf'], tritium_data['maxf'], nbins), 'fss_bins': False # If fss_bins is True, bins is ignored and overridden } @@ -210,7 +211,7 @@ def SaveUnbinnedData(tritium_data, root_file="./results/tritium_analysis.root"): -def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/tritium_analysis.root', stan_files_location='../../morpho_models/', model_code='tritium_model/models/tritium_phase_II_analyzer_binned.stan', scattering_params_R='simplified_scattering_params.R'): +def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/tritium_analysis.root', stan_files_location='/host/morpho_models/', model_code='tritium_model/models/tritium_phase_II_analyzer_binned.stan', scattering_params_R='/host/simplified_scattering_params.R'): """ Analyzes frequency or kinetic energy data using a Stan model. Saves and plots posteriors. diff --git a/test_analysis/fake_data_stan_analysis_termite.py b/test_analysis/fake_data_stan_analysis_termite.py new file mode 100644 index 00000000..36db704b --- /dev/null +++ b/test_analysis/fake_data_stan_analysis_termite.py @@ -0,0 +1,513 @@ +# +# fake_data_stan_analysis.py +# Author: T. E. Weiss +# Date modified: June 2, 2020 +# +# This script generates fake data, then analyzes the data in Stan to infer posteriors. +# Pathnames configured for running from: termite/phase2_main_scripts/ +# + +""" +To-do: + - In FakeExperimentEnsemble, add: + 1. Tracking of convergence issues, so that a summary of problems can be saved/printed + 2. An option to parallelize with slurm instead of multiprocessing + - Run morpho processor for ensemble-analysis plotting, once it has been tested sufficiently +""" + + +#import unittest +from morpho.utilities import morphologging +logger = morphologging.getLogger(__name__) +import logging +import numpy as np +import time +import argparse +parser = argparse.ArgumentParser() + +from scipy.interpolate import interp1d +import json + + +#Importing constants from mermithid +from mermithid.misc.Constants import * +from mermithid.processors.TritiumSpectrum.FakeDataGenerator import FakeDataGenerator +from mermithid.processors.misc.TritiumAndEfficiencyBinner import TritiumAndEfficiencyBinner +#Importing processors from morpho +from morpho.processors.sampling import PyStanSamplingProcessor, PriorSamplingProcessor +from morpho.processors.plots import Histogram, APosterioriDistribution, Histo2dDivergence +from morpho.processors.IO import IOROOTProcessor, IORProcessor + +#Defining processors +priorSampler = PriorSamplingProcessor("sample") +specGen = FakeDataGenerator("specGen") +writerProcessor = IOROOTProcessor("writer") +rReaderProcessor = IORProcessor("reader") +analysisProcessor = PyStanSamplingProcessor("analyzer") +histPlotter = Histogram("histo") +aposterioriPlotter = APosterioriDistribution("posterioriDistrib") +divPlotter = Histo2dDivergence("2dDivergence") + + + +def DefineGeneratorInputs(root_file='./results/tritium_analysis.root'): + """ + Samples inputs to a fake data generator from priors, then combines them in a dictionary with fixed inputs to the generator. Saves all the inputs to a root file. + + Returns: + generator_inputs: dictionary of all inputs to a fake data generator + """ + prior_sampler_config = { + "fixed_inputs": { + 'Nscatters': const_dict['Nscatters_generation'], + 'minf': const_dict['minf'], #In Hz + 'err_from_B': const_dict['err_from_B_generation'], + 'H2_scatter_prop': const_dict['H2_scatter_prop_tritium'], + }, + "priors": [ + {'name': 'Q', 'prior_dist': 'normal', 'prior_params': const_dict['Q']}, + {'name': 'mass', 'prior_dist': 'gamma', 'prior_params': const_dict['mass']}, + {'name': 'sigma', 'prior_dist': 'normal', 'prior_params': const_dict['sigma']}, #From Ali's complex lineshape fits. Final states also included + {'name': 'S', 'prior_dist': 'poisson', 'prior_params': const_dict['S']}, + {'name': 'B_1kev', 'prior_dist': 'lognormal', 'prior_params': const_dict['B_1kev']}, + {'name': 'survival_prob', 'prior_dist': 'beta', 'prior_params': const_dict['survival_prob']}, #Centered around 0.736. To be replaced given complex lineshape result/systematic assessment + {'name': 'Bfield', 'prior_dist': 'normal', 'prior_params': const_dict['Bfield']}, #From complex lineshape fit to calibration data. More sig figs on mean needed? + ] + } + + inputs_writer_config = { + "action": "write", + "tree_name": "input", + "filename": root_file, + "variables": [ + {"variable": "Nscatters", "type":"int"}, + {"variable": "minf", "type": "float"}, + {"variable": "err_from_B", "type": "float"}, + {"variable": "Q", "type": "float"}, + {"variable": "mass", "type": "float"}, + {"variable": "sigma", "type": "float"}, + {"variable": "S", "type": "float"}, + {"variable": "B_1kev", "type": "float"}, + {"variable": "survival_prob", "type": "float"}, + {"variable": "Bfield", "type": "float"} + ]} + + #Configuration step + priorSampler.Configure(prior_sampler_config) + writerProcessor.Configure(inputs_writer_config) + + #Sampling inputs + priorSampler.Run() + generator_inputs = priorSampler.results + + #Saving results + gen_inputs_root = {key:[value] for key, value in generator_inputs.items()} + writerProcessor.data = gen_inputs_root + writerProcessor.Run() + + return generator_inputs + + +def GenerateFakeData(inputs_dict): + """ + Generates fake Phase II tritium beta spectrum data and plots it. + + Arguments: + - inputs_dict: dictionary of parameter values inputted to the fake data generator. + + Returns: + - results: dict with + 1) keys: K (energy), F (frequency) + 2) mapped values: energy, frequency + """ + specGen_config = { + "apply_efficiency": True, + "efficiency_path": "/host-termite/analysis_input/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + "detailed_or_simplified_lineshape": "detailed", + "return_frequency": True, + "Q": inputs_dict["Q"], + "mass": inputs_dict["mass"], + "minf": inputs_dict["minf"], + "scattering_sigma": inputs_dict["sigma"], + "S": inputs_dict["S"], + "B_1kev": inputs_dict["B_1kev"], + "survival_prob": inputs_dict["survival_prob"], + "err_from_B": inputs_dict["err_from_B"], + "Nscatters": inputs_dict["Nscatters"], + "B_field": inputs_dict["Bfield"], + "scatter_proportion": inputs_dict["H2_scatter_prop"], + "n_steps": 100000, + } + + histo_config = { + "variables": "F", + "n_bins_x": 65, + "output_path": "./results/", + "title": "psuedo-data"+str(inputs_dict['Q']), + "format": "pdf" + } + + #Configuration step + specGen.Configure(specGen_config) + histPlotter.Configure(histo_config) + #Generate data + specGen.Run() + results = specGen.results + #Plot histograms of generated data + histPlotter.data = {'F':results['F'].tolist()} + histPlotter.Run() + + return results + + +def BinAndSaveData(tritium_data, nbins, root_file="./results/tritium_analysis.root"): + eff_path = "/host-termite/analysis_input/combined_energy_corrected_eff_at_quad_trap_frequencies.json" + + binner_config = { + "energy_or_frequency": 'frequency', + "variables": "F", + "title": "corrected_spectrum", + "efficiency_filepath": eff_path, + 'bins': np.linspace(tritium_data['minf'], tritium_data['maxf'], nbins), #(tritium_data['maxf']-tritium_data['minf'])/float(nbins) + 'fss_bins': False # If fss_bins is True, bins is ignored and overridden + } + + binner = TritiumAndEfficiencyBinner("binner") + binner.Configure(binner_config) + binner.data = tritium_data + binner.Run() + results = binner.results + + eff_means = results['bin_efficiencies'] + eff_errs = (results['bin_efficiency_errors'][0]+results['bin_efficiency_errors'][1])/2. + for i in range(len(eff_means)): + if (eff_means[i]K conversion + "Bfield_ctr": const_dict['Bfield'][0], #From complex lineshape fit to calibration + "Bfield_std": const_dict['Bfield'][1], #data. More sig figs on mean needed? + "survival_prob_alpha": const_dict['survival_prob'][0], #Centered around ~0.736. To be replaced + "survival_prob_beta": const_dict['survival_prob'][1], #given complex lineshape result+systematics + "Q_ctr": QT2(), + "Q_std": const_dict['Q_std_analysis'], + "m_alpha": const_dict['mass'][0], + "m_beta": 1./const_dict['mass'][1], + "B_1kev_logctr": const_dict['B_1kev'][0], + "B_1kev_logstd": const_dict['B_1kev'][1], + "KEscale": const_dict['KEscale'], #This enables the option of cmdstan running +# "slope": 0.000390369173, #For efficiency modeling with unbinned data +# "intercept": -6.00337656, + "Nscatters": const_dict['Nscatters_analysis'] #Because peaks>16 in simplified linesahpe have means->inf as FWHM->0 + }, + "interestParams": interest_params, + } + + + vars_to_save = [ + {"variable": "Q", "type": "float"}, + {"variable": "mass", "type": "float"}, + {"variable": "survival_prob", "type": "float"}, + {"variable": "Bfield", "type": "float"}, + {"variable": "sigma", "type": "float"}, + {"variable": "S", "type": "float"}, + {"variable": "B_1kev", "type": "float"}, + {"variable": "B", "type": "float"}, + {"variable": "KEmin", "type": "float"}, + {"variable": "Ndata_gen", "type": "float"}, + {"variable": "rate_param", "type": "float"}, + {"variable": "KE_sample", "type": "float"}, + {"variable": "Nfit_signal", "type": "float"}, + {"variable": "Nfit_bkgd", "type": "float"}, + {"variable": "divergent__", "root_alias": "divergence", "type": "float"}, + {"variable": "energy__", "root_alias": "energy", "type": "float"}, + {"variable": "lp_prob", "root_alias": "lp_prob", "type": "float"} + ] + + for i in range(Nbins): + vars_to_save.append({"variable": 'Nfit_bins[{}]'.format(str(i)), "root_alias": 'Nfit_bins{}'.format(str(i)), "type": "float"}) + + + posteriors_writer_config = { + "action": "write", + "tree_name": "analysis", + "file_option": "update", + "filename": root_file, + "variables": vars_to_save} + + #Configuration step + rReaderProcessor.Configure(scattering_reader_config) + analysisProcessor.Configure(analyzer_config) + writerProcessor.Configure(posteriors_writer_config) + + #Make data accessible to analyzer + analysisProcessor.data = tritium_data + analysisProcessor.data = {'Nbins': Nbins} + + #Make scattering parameters accessible to analyzer + rReaderProcessor.Run() + pi = rReaderProcessor.data + analysisProcessor.data = {key:val[:analysisProcessor.data['Nscatters']] for key, val in pi.items()} + + #Run analysis + analysisProcessor.Run() + results = analysisProcessor.results + + #Save results + writerProcessor.data = results + writerProcessor.Run() + + return results + + +def PlotStanResults(posteriors, correlation_vars=['Q', 'mass'], divergence_vars=['Q', 'Bfield', 'survival_prob']): + """ + Creates and saves two plots: + a) posteriors and correlations between them, and + b) plot showing where in parameter space Hamiltonian Monte Carlo divergences occured. + + Required argument: + 1) posteriors: dict; output of the PyStanSamplingProcessor + Optional arguments: + 2) correlation_vars: list of strings; names of variables to be included in correlation plot + 3) divergence_vars: list of strings; names of variables to be included in divergences plot + """ + aposteriori_config = { + "n_bins_x": 50, #Potentially increase + "n_bins_y": 50, + "variables": correlation_vars, + "title": "Q_vs_Kmin", + "output_path": "./plots" + } + + div_plot_config = { + "n_bins_x": 50, + "n_bins_y": 50, + "variables": divergence_vars, + "title": "div_plot", + "output_path": "./plots" + } + + #Configuration step + aposterioriPlotter.Configure(aposteriori_config) + divPlotter.Configure(div_plot_config) + + #Plots correlations between posteriors + aposterioriPlotter.data = posteriors + aposterioriPlotter.Run() + + #Plot 2D grid of divergent points + divPlotter.data = posteriors + divPlotter.Run() + + +def PerformFakeExperiment(root_filename, plot_results=False, parallelized=True, bin_data=True, wait=45): + """ + Generate fake tritium data and analyze it in Stan. If plot_results==True, create correlation and divergence plots. + + Saves generation inputs, generation results, and analysis results to different trees of one ROOT file. + """ + if parallelized==True: + flist = root_filename.split('/') + run_num = float(flist[len(flist)-1][0]) + time.sleep(run_num*wait) #Optionally stagger the runs slightly, either for debugging/clarity of output, or to avoid any possible memory overflows + logger.info("----------------------MORPHO RUN #{}----------------------".format(int(run_num))) + + + #Sample inputs to the data generator and save to one branch of a root file: + inputs_dict = DefineGeneratorInputs(root_filename) + + #Generate data using the inputs + tritium_data_unbinned = GenerateFakeData(inputs_dict) + + #Optionally bin data, then save it + if bin_data: + nbins = const_dict['f_nbins'] + tritium_data = BinAndSaveData(tritium_data_unbinned, nbins, root_filename) + else: + tritium_data = SaveUnbinnedData(tritium_data, root_filename) + + #Analyze data and save posteriors + posteriors = StanTritiumAnalysis(tritium_data, root_file=root_filename) + + #Optionally plot posteriors + if plot_results == True: + PlotStanResults(posteriors) + + +def CalibrateResults(root_filenames, vars_to_calibrate, cred_interval=[0.05, 0.95]): + from morpho.processors.diagnostics.CalibrationProcessor import CalibrationProcessor + calibrator = CalibrationProcessor("calib") + + calib_config = { + "files": root_filenames, + "in_param_names": vars_to_calibrate, + "cred_interval": cred_interval, + "quantiles": True + } + #Configuration step + check_success = calibrator.Configure(calib_config) + if check_success == False: + return + + calibrator.Run() + + +def FakeExperimentEnsemble(n_runs, root_basename, parallelize=True, n_processes=4, vars_to_calibrate=['Q']): + """ + To-do: add parallelization option for a Slurm environment. + + n_runs: int; number of pseudo-experiments to be performed + root_basename: str; results are saved in rootfiles labeled by root_basename and the pseudo-experiment number + """ + if n_runs==1: + logger.info("PERFORMING 1 MORPHO PSEUDO-EXPERIMENT") + else: + logger.info("PERFORMING {} MORPHO PSEUDO-EXPERIMENTS".format(n_runs)) + + + if parallelize == False: + root_filenames = [] + for i in range(n_runs): + logger.info("----------MORPHO RUN #{}----------".format(i)) + temp_root_name = "./results/"+str(i)+root_basename + root_filenames.append(temp_root_name) + PerformFakeExperiment(temp_root_name) + else: + from multiprocessing import Pool + root_filenames = ["./results/"+str(i)+root_basename for i in range(n_runs)] + with Pool(n_processes) as p: + p.map(PerformFakeExperiment, root_filenames) + + #coverages = CalibrateResults(root_filenames, vars_to_calibrate) + coverages = None + return coverages + + +if __name__ == '__main__': + const_dict = { + # physical + 'Q':[QT2(),0.07], 'Q_std_analysis':75, 'mass':[1.1532, 0.4291], 'mass_init':0.2, + # instrumental + 'sigma': [15.524869238312053, 2.1583740056146], #[14.542692695653235, 1.3080022178297848], #1.301788807656317 + 'S':[3594], + 'err_from_B_generation':0, 'err_from_B_analysis':0.001, + 'B_1kev':[-3.057997933394048, 1.9966097834821164], 'B_1kev_init':0.0469817, 'B_init':0.06, + 'minf':1353.125e+06 - 40e+06 + 24.5e+09, 'f_nbins':65, + # complex lineshape + 'survival_prob_mean': 0.672621806411212, #0.688, #0.736, + 'survival_prob_err': 0.10760576977058844, #0.0368782, #0.00850261, + 'survival_prob': [12.118849968055722, 5.898481394892654], #[107.90268235294104, 48.93261176470582], #[2042.1165325779257, 926.0761019830128] + 'Bfield': [0.957809551203932, 8.498072412705302e-6], #[0.957805552, 1.3926736876423273e-6], #[0.957805552, 7.620531477410062e-7] + 'Nscatters_generation':20, 'Nscatters_analysis':16, + 'H2_scatter_prop_tritium':1., + # Stan fit + 'chain':1, 'warmup':2000, 'iter':3000, + 'KEscale':16323, 'adapt_delta':0.8 + } + + interest_vars = ['Q', 'mass','survival_prob', 'Bfield', 'sigma', 'S', 'B_1kev'] + + parser.add_argument("root_filename", type=str) + args = parser.parse_args() + + #CalibrateResults([args.root_filename], interest_vars, [0.16, 0.84]) + FakeExperimentEnsemble(3, args.root_filename, vars_to_calibrate=interest_vars) +