aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xutils/plot-fit-results293
1 files changed, 293 insertions, 0 deletions
diff --git a/utils/plot-fit-results b/utils/plot-fit-results
new file mode 100755
index 0000000..b568075
--- /dev/null
+++ b/utils/plot-fit-results
@@ -0,0 +1,293 @@
+#!/usr/bin/env python
+from __future__ import print_function, division
+import yaml
+import numpy as np
+from scipy.stats import iqr
+from matplotlib.lines import Line2D
+
+# on retina screens, the default plots are way too small
+# by using Qt5 and setting QT_AUTO_SCREEN_SCALE_FACTOR=1
+# Qt5 will scale everything using the dpi in ~/.Xresources
+import matplotlib
+matplotlib.use("Qt5Agg")
+
+matplotlib.rc('font', size=22)
+
+IDP_E_MINUS = 20
+IDP_MU_MINUS = 22
+
+SNOMAN_MASS = {
+ 20: 0.511,
+ 21: 0.511,
+ 22: 105.658,
+ 23: 105.658
+}
+
+def plot_hist(x, label=None):
+ # determine the bin width using the Freedman Diaconis rule
+ # see https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule
+ h = 2*iqr(x)/len(x)**(1/3)
+ n = max(int((np.max(x)-np.min(x))/h),10)
+ bins = np.linspace(np.min(x),np.max(x),n)
+ plt.hist(x, bins=bins, histtype='step', label=label)
+
+def plot_legend(n):
+ plt.figure(n)
+ ax = plt.gca()
+ handles, labels = ax.get_legend_handles_labels()
+ new_handles = [Line2D([],[],c=h.get_edgecolor()) for h in handles]
+ plt.legend(handles=new_handles,labels=labels)
+
+def get_stats(x):
+ """
+ Returns a tuple (mean, error mean, std, error std) for the values in x.
+
+ The formula for the standard error on the standard deviation comes from
+ https://stats.stackexchange.com/questions/156518.
+ """
+ mean = np.mean(x)
+ std = np.std(x)
+ n = len(x)
+ u4 = np.mean((x-mean)**4)
+ error = np.sqrt((u4-(n-3)*std**4/(n-1))/n)/(2*std)
+ return mean, std/np.sqrt(n), std, error
+
+if __name__ == '__main__':
+ import argparse
+ import matplotlib.pyplot as plt
+ import numpy as np
+
+ parser = argparse.ArgumentParser("plot fit results")
+ parser.add_argument("filenames", nargs='+', help="input files")
+ args = parser.parse_args()
+
+ events = []
+
+ for filename in args.filenames:
+ print(filename)
+ with open(filename) as f:
+ data = yaml.load(f.read())
+
+ a = np.ma.empty(len(data['data']),
+ dtype=[('id',np.int), # particle id
+ ('T', np.double), # true energy
+ ('dx',np.double), # dx
+ ('dy',np.double), # dy
+ ('dz',np.double), # dz
+ ('dT',np.double), # dT
+ ('theta',np.double), # theta
+ ('ratio',np.double), # likelihood ratio
+ ('te',np.double), # time electron
+ ('tm',np.double), # time muon
+ ('Te',np.double)] # electron energy
+ )
+
+ for i, event in enumerate(data['data']):
+ # get the particle ID
+ id = event['mctk'][-1]['id']
+
+ a[i]['id'] = id
+
+ if id not in event['ev'][0]['fit']:
+ a[i] = np.ma.masked
+ continue
+
+ mass = SNOMAN_MASS[id]
+ # for some reason it's the *second* track which seems to contain the
+ # initial energy
+ true_energy = event['mctk'][-1]['energy']
+ # The MCTK bank has the particle's total energy (except for neutrons)
+ # so we need to convert it into kinetic energy
+ ke = true_energy - mass
+
+ fit = event['ev'][0]['fit']
+
+ a[i]['T'] = ke
+ energy = fit[id]['energy']
+ a[i]['dT'] = energy-ke
+
+ # store the fit position residuals
+ true_posx = event['mcvx'][0]['posx']
+ posx = fit[id]['posx']
+ a[i]['dx'] = posx-true_posx
+ true_posy = event['mcvx'][0]['posy']
+ posy = fit[id]['posy']
+ a[i]['dy'] = posy-true_posy
+ true_posz = event['mcvx'][0]['posz']
+ posz = fit[id]['posz']
+ a[i]['dz'] = posz-true_posz
+
+ # compute the angle between the fit direction and the true
+ # direction
+ dirx = event['mctk'][-1]['dirx']
+ diry = event['mctk'][-1]['diry']
+ dirz = event['mctk'][-1]['dirz']
+ true_dir = [dirx,diry,dirz]
+ true_dir = np.array(true_dir)/np.linalg.norm(true_dir)
+ theta = fit[id]['theta']
+ phi = fit[id]['phi']
+ dir = [np.sin(theta)*np.cos(phi),np.sin(theta)*np.sin(phi),np.cos(theta)]
+ dir = np.array(dir)/np.linalg.norm(dir)
+ a[i]['theta'] = np.degrees(np.arccos(np.dot(true_dir,dir)))
+
+ # compute the log likelihood ratio
+ if IDP_E_MINUS in fit and IDP_MU_MINUS in fit:
+ fmin_electron = fit[IDP_E_MINUS]['fmin']
+ fmin_muon = fit[IDP_MU_MINUS]['fmin']
+ a[i]['ratio'] = fmin_muon-fmin_electron
+ else:
+ a[i]['ratio'] = np.ma.masked
+
+ # store the time taken for electron and muon fits
+ if IDP_E_MINUS in fit:
+ a[i]['te'] = fit[IDP_E_MINUS]['time']
+ a[i]['Te'] = fit[IDP_E_MINUS]['energy']
+ else:
+ a[i]['te'] = np.ma.masked
+ a[i]['Te'] = np.ma.masked
+ if IDP_MU_MINUS in fit:
+ a[i]['tm'] = fit[IDP_MU_MINUS]['time']
+ else:
+ a[i]['tm'] = np.ma.masked
+
+ events.append(a)
+
+ a = np.concatenate(events)
+
+ bins = np.arange(50,1000,100)
+
+ stats_array = np.ma.empty(len(bins)-1,
+ dtype=[('T', np.double),
+ ('dT', np.double),
+ ('dT_err', np.double),
+ ('dT_std', np.double),
+ ('dT_std_err', np.double),
+ ('dx', np.double),
+ ('dx_err', np.double),
+ ('dx_std', np.double),
+ ('dx_std_err', np.double),
+ ('dy', np.double),
+ ('dy_err', np.double),
+ ('dy_std', np.double),
+ ('dy_std_err', np.double),
+ ('dz', np.double),
+ ('dz_err', np.double),
+ ('dz_std', np.double),
+ ('dz_std_err', np.double),
+ ('theta', np.double),
+ ('theta_err', np.double),
+ ('theta_std', np.double),
+ ('theta_std_err', np.double)])
+
+ stats = {IDP_E_MINUS: stats_array, IDP_MU_MINUS: stats_array.copy()}
+
+ for id in stats:
+ electron_events = a[a['id'] == id]
+
+ for i, (ablah, b) in enumerate(zip(bins[:-1], bins[1:])):
+ events = electron_events[(electron_events['T'] >= ablah) & (electron_events['T'] < b)]
+
+ if len(events) < 2:
+ stats[id][i] = np.ma.masked
+ continue
+
+ stats[id][i]['T'] = (ablah+b)/2
+ mean, mean_error, std, std_error = get_stats(events['dT'].compressed())
+ stats[id][i]['dT'] = mean
+ stats[id][i]['dT_err'] = mean_error
+ stats[id][i]['dT_std'] = std
+ stats[id][i]['dT_std_err'] = std_error
+ mean, mean_error, std, std_error = get_stats(events['dx'].compressed())
+ stats[id][i]['dx'] = mean
+ stats[id][i]['dx_err'] = mean_error
+ stats[id][i]['dx_std'] = std
+ stats[id][i]['dx_std_err'] = std_error
+ mean, mean_error, std, std_error = get_stats(events['dy'].compressed())
+ stats[id][i]['dy'] = mean
+ stats[id][i]['dy_err'] = mean_error
+ stats[id][i]['dy_std'] = std
+ stats[id][i]['dy_std_err'] = std_error
+ mean, mean_error, std, std_error = get_stats(events['dz'].compressed())
+ stats[id][i]['dz'] = mean
+ stats[id][i]['dz_err'] = mean_error
+ stats[id][i]['dz_std'] = std
+ stats[id][i]['dz_std_err'] = std_error
+ mean, mean_error, std, std_error = get_stats(events['theta'].compressed())
+ stats[id][i]['theta'] = mean
+ stats[id][i]['theta_err'] = mean_error
+ stats[id][i]['theta_std'] = std
+ stats[id][i]['theta_std_err'] = std_error
+
+ for id in stats:
+ label = 'Muon' if id == IDP_MU_MINUS else 'Electron'
+
+ T = stats[id]['T']
+ dT = stats[id]['dT']
+ dT_err = stats[id]['dT_err']
+ std_dT = stats[id]['dT_std']
+ std_dT_err = stats[id]['dT_std_err']
+ dx = stats[id]['dx']
+ dx_err = stats[id]['dx_err']
+ std_dx = stats[id]['dx_std']
+ std_dx_err = stats[id]['dx_std_err']
+ dy = stats[id]['dy']
+ dy_err = stats[id]['dy_err']
+ std_dy = stats[id]['dy_std']
+ std_dy_err = stats[id]['dy_std_err']
+ dz = stats[id]['dz']
+ dz_err = stats[id]['dz_err']
+ std_dz = stats[id]['dz_std']
+ std_dz_err = stats[id]['dz_std_err']
+ theta = stats[id]['theta']
+ theta_err = stats[id]['theta_err']
+ std_theta = stats[id]['theta_std']
+ std_theta_err = stats[id]['theta_std_err']
+
+ plt.figure(1)
+ plt.errorbar(T,dT*100/T,yerr=dT_err*100/T,fmt='o',label=label)
+ plt.xlabel("Kinetic Energy (MeV)")
+ plt.ylabel("Energy bias (%)")
+ plt.title("Energy Bias")
+ plt.legend()
+
+ plt.figure(2)
+ plt.errorbar(T,std_dT*100/T,yerr=std_dT_err*100/T,fmt='o',label=label)
+ plt.xlabel("Kinetic Energy (MeV)")
+ plt.ylabel("Energy resolution (%)")
+ plt.title("Energy Resolution")
+ plt.legend()
+
+ plt.figure(3)
+ plt.errorbar(T,dx,yerr=dx_err,fmt='o',label='%s (x)' % label)
+ plt.errorbar(T,dy,yerr=dy_err,fmt='o',label='%s (y)' % label)
+ plt.errorbar(T,dz,yerr=dz_err,fmt='o',label='%s (z)' % label)
+ plt.xlabel("Kinetic Energy (MeV)")
+ plt.ylabel("Position bias (cm)")
+ plt.title("Position Bias")
+ plt.legend()
+
+ plt.figure(4)
+ plt.errorbar(T,std_dx,yerr=std_dx_err,fmt='o',label='%s (x)' % label)
+ plt.errorbar(T,std_dy,yerr=std_dy_err,fmt='o',label='%s (y)' % label)
+ plt.errorbar(T,std_dz,yerr=std_dz_err,fmt='o',label='%s (z)' % label)
+ plt.xlabel("Kinetic Energy (MeV)")
+ plt.ylabel("Position resolution (cm)")
+ plt.title("Position Resolution")
+ plt.ylim((0,plt.gca().get_ylim()[1]))
+ plt.legend()
+
+ plt.figure(5)
+ plt.errorbar(T,std_theta,yerr=std_theta_err,fmt='o',label=label)
+ plt.xlabel("Kinetic Energy (MeV)")
+ plt.ylabel("Angular resolution (deg)")
+ plt.title("Angular Resolution")
+ plt.ylim((0,plt.gca().get_ylim()[1]))
+ plt.legend()
+
+ plt.figure(6)
+ plt.scatter(a[a['id'] == id]['Te'],a[a['id'] == id]['ratio'],label=label)
+ plt.xlabel("Reconstructed Electron Energy (MeV)")
+ plt.ylabel(r"Log Likelihood Ratio (e/$\mu$)")
+ plt.title("Log Likelihood Ratio vs Reconstructed Electron Energy")
+ plt.legend()
+ plt.show()