#!/usr/bin/env python # Copyright (c) 2019, Anthony Latorre # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . from __future__ import print_function, division import yaml try: from yaml import CLoader as Loader except ImportError: from yaml.loader import SafeLoader as Loader 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(),Loader=Loader) 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['mcgn'][0]['id'] a[i]['id'] = id if 'fit' not in event['ev'][0]: # if nhit < 100 we don't fit the event continue 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['mcgn'][0]['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['mcgn'][0]['posx'] posx = fit[id]['posx'] a[i]['dx'] = posx-true_posx true_posy = event['mcgn'][0]['posy'] posy = fit[id]['posy'] a[i]['dy'] = posy-true_posy true_posz = event['mcgn'][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['mcgn'][0]['dirx'] diry = event['mcgn'][0]['diry'] dirz = event['mcgn'][0]['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.ma.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()