aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--utils/.gitignore2
-rwxr-xr-xutils/apply-atmospheric-oscillations322
-rwxr-xr-xutils/calculate-atmospheric-oscillations116
-rwxr-xr-xutils/plot-atmospheric-fluxes285
-rwxr-xr-xutils/plot-atmospheric-oscillations114
6 files changed, 841 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 25e9f86..102db0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
*.swp
*.swo
+*.swm
+*.swn
*.o
*.zdab
*.txt
diff --git a/utils/.gitignore b/utils/.gitignore
new file mode 100644
index 0000000..e307f16
--- /dev/null
+++ b/utils/.gitignore
@@ -0,0 +1,2 @@
+*.eps
+*.pdf
diff --git a/utils/apply-atmospheric-oscillations b/utils/apply-atmospheric-oscillations
new file mode 100755
index 0000000..3d9f32d
--- /dev/null
+++ b/utils/apply-atmospheric-oscillations
@@ -0,0 +1,322 @@
+#!/usr/bin/env python
+"""
+Script to combine the low energy Battistoni atmospheric flux with the high
+energy flux from Giles Barr and apply oscillations.
+
+Note: There are a couple of potential issues with this:
+
+1. The Battistoni fluxes give a total flux integrated over the zenith angle
+whereas we need to account for the zenith dependence. Therefore, we just use
+the zenith dependence of the lowest energy in the Barr flux files.
+
+2. I only have access to the Battistoni fluxes for electron neutrinos.
+Therefore I will assume that the muon neutrino flux is twice as big which seems
+to be roughly correct based on the paper "The Atmospheric Neutrino Flux Below
+100 MeV: The FLUKA Results".
+
+To run it:
+
+ $ ./apply-atmospheric-oscillations --flux-dir ~/atm-production/fluxes/irc01_plus_lowe \
+ --osc-dir ~/atm-production/nucraft_osc
+"""
+from __future__ import print_function, division
+import numpy as np
+
+def pdg_code_to_string(pdg):
+ if pdg == 12:
+ return "nue"
+ elif pdg == -12:
+ return "nbe"
+ elif pdg == 14:
+ return "num"
+ elif pdg == -14:
+ return "nbm"
+ elif pdg == 16:
+ return "nut"
+ elif pdg == -16:
+ return "nbt"
+ else:
+ raise ValueError("Unknown pdg code %i" % pdg)
+
+if __name__ == '__main__':
+ import argparse
+ import matplotlib.pyplot as plt
+ from os.path import split, splitext, join
+ from scipy.interpolate import RectBivariateSpline
+ from scipy.integrate import dblquad, nquad
+ import sys
+
+ parser = argparse.ArgumentParser("combine low energy and high energy atmospheric fluxes and apply oscillations")
+ parser.add_argument("--flux-dir",required=True,help="atmospheric production directory")
+ parser.add_argument("--osc-dir",required=True,help="directory with atmospheric oscillations")
+ args = parser.parse_args()
+
+ # Energy and cos(zenith) bins for the fluxes from Barr
+ barr_cos_theta_bins = np.linspace(-1,1,21)
+ barr_energy_bins = np.logspace(-1,1,41)
+
+ # Final energy bins
+ # We go here from 10 MeV -> 10 GeV with 20 points per decade like the Barr
+ # fluxes
+ energy_bins = np.logspace(-2,1,61)
+ cos_theta_bins = barr_cos_theta_bins.copy()
+
+ # Read in Battistoni fluxes
+ nue_battistoni = np.genfromtxt(join(args.flux_dir,"lowe/nue.dat"))
+ nbe_battistoni = np.genfromtxt(join(args.flux_dir,"lowe/nbe.dat"))
+
+ # Convert Battistoni fluxes from MeV -> GeV
+ nue_battistoni[:,0] /= 1000.0
+ nue_battistoni[:,1] *= 1000.0
+ nbe_battistoni[:,0] /= 1000.0
+ nbe_battistoni[:,1] *= 1000.0
+
+ # Convert Battistoni fluxes from cm^-2 -> m^-2
+ nue_battistoni[:,1] *= 100.0**2
+ nbe_battistoni[:,1] *= 100.0**2
+
+ # Assume muon neutrino flux below 100 MeV is twice that of electron
+ # neutrino flux
+ num_battistoni = nue_battistoni.copy()
+ num_battistoni[:,1] *= 2
+ nbm_battistoni = nbe_battistoni.copy()
+ nbm_battistoni[:,1] *= 2
+
+ # Read in Barr fluxes
+ nue_barr = np.genfromtxt(join(args.flux_dir,"irc01/fmax20_i0403z.sno_nue"))
+ nbe_barr = np.genfromtxt(join(args.flux_dir,"irc01/fmax20_i0403z.sno_nbe"))
+ num_barr = np.genfromtxt(join(args.flux_dir,"irc01/fmax20_i0403z.sno_num"))
+ nbm_barr = np.genfromtxt(join(args.flux_dir,"irc01/fmax20_i0403z.sno_nbm"))
+
+ # Read in oscillation probabilities
+ nue_osc_prob = np.genfromtxt(join(args.osc_dir,"nue_osc_prob.txt"))
+ nbe_osc_prob = np.genfromtxt(join(args.osc_dir,"nbe_osc_prob.txt"))
+ num_osc_prob = np.genfromtxt(join(args.osc_dir,"num_osc_prob.txt"))
+ nbm_osc_prob = np.genfromtxt(join(args.osc_dir,"nbm_osc_prob.txt"))
+
+ # Reshape oscillation probability arrays. They should now look like:
+ #
+ # [[[0.01, -1, P(nue), P(num), P(nut)],
+ # [0.01, -0.8, P(nue), P(num), P(nut)],
+ # ...
+ # [0.01, 1.0, P(nue), P(num), P(nut)]],
+ # [[0.02, -1, P(nue), P(num), P(nut)],
+ # [0.02, -0.8, P(nue), P(num), P(nut)],
+ # ...
+ # [0.02, 1.0, P(nue), P(num), P(nut)]],
+ # ...
+ # [[10.0, -1, P(nue), P(num), P(nut)],
+ # [10.0, -0.8, P(nue), P(num), P(nut)],
+ # ...
+ # [10.0, 1.0, P(nue), P(num), P(nut)]]]
+ shape0 = len(np.unique(nue_osc_prob[:,0]))
+ nue_osc_prob = nue_osc_prob.reshape((shape0,-1,5))
+ nbe_osc_prob = nbe_osc_prob.reshape((shape0,-1,5))
+ num_osc_prob = num_osc_prob.reshape((shape0,-1,5))
+ nbm_osc_prob = nbm_osc_prob.reshape((shape0,-1,5))
+
+ # convert dn/dlnE -> dn/dE
+ nue_barr[:,2] /= nue_barr[:,0]
+ nbe_barr[:,2] /= nbe_barr[:,0]
+ num_barr[:,2] /= num_barr[:,0]
+ nbm_barr[:,2] /= nbm_barr[:,0]
+
+ # Reshape Barr flux arrays. They should now look like:
+ #
+ # [[[0.1, -1, flux,...],
+ # [0.2, -1, flux,...],
+ # ...
+ # [10.0, -1, flux,...]],
+ #
+ # [[0.1, -0.9, flux,...],
+ # [0.2, -0.9, flux,...],
+ # ...
+ # [10.0, -0.9, flux,...]],
+ # ...
+ # ...
+ # [[0.1, 1.0, flux,...],
+ # [0.2, 1.0, flux,...],
+ # ...
+ # [10.0, 1.0, flux,...]]]
+ shape1 = len(np.unique(nue_barr[:,0]))
+ nue_barr = nue_barr.reshape((-1,shape1,5))
+ nbe_barr = nbe_barr.reshape((-1,shape1,5))
+ num_barr = num_barr.reshape((-1,shape1,5))
+ nbm_barr = nbm_barr.reshape((-1,shape1,5))
+
+ nue_zenith_dist_x = nue_barr[:,0,1].copy()
+ nue_zenith_dist_y = nue_barr[:,0,2].copy()
+ nue_zenith_dist_y /= np.trapz(nue_zenith_dist_y,x=nue_zenith_dist_x)
+ nbe_zenith_dist_x = nbe_barr[:,0,1].copy()
+ nbe_zenith_dist_y = nbe_barr[:,0,2].copy()
+ nbe_zenith_dist_y /= np.trapz(nbe_zenith_dist_y,x=nbe_zenith_dist_x)
+ num_zenith_dist_x = num_barr[:,0,1].copy()
+ num_zenith_dist_y = num_barr[:,0,2].copy()
+ num_zenith_dist_y /= np.trapz(num_zenith_dist_y,x=num_zenith_dist_x)
+ nbm_zenith_dist_x = nbm_barr[:,0,1].copy()
+ nbm_zenith_dist_y = nbm_barr[:,0,2].copy()
+ nbm_zenith_dist_y /= np.trapz(nbm_zenith_dist_y,x=nbm_zenith_dist_x)
+
+ nue_battistoniz = np.empty((nue_barr.shape[0],nue_battistoni.shape[0],4))
+ nue_battistoniz[:,:,0] = nue_battistoni[:,0]
+ nue_battistoniz[:,:,1] = nue_zenith_dist_x[:,np.newaxis]
+ nue_battistoniz[:,:,2] = nue_battistoni[:,1]*nue_zenith_dist_y[:,np.newaxis]/(2*np.pi)
+ nue_battistoniz[:,:,3] = nue_battistoniz[:,:,2]*nue_battistoniz[:,:,0]
+
+ nbe_battistoniz = np.empty((nbe_barr.shape[0],nbe_battistoni.shape[0],4))
+ nbe_battistoniz[:,:,0] = nbe_battistoni[:,0]
+ nbe_battistoniz[:,:,1] = nbe_zenith_dist_x[:,np.newaxis]
+ nbe_battistoniz[:,:,2] = nbe_battistoni[:,1]*nbe_zenith_dist_y[:,np.newaxis]/(2*np.pi)
+ nbe_battistoniz[:,:,3] = nbe_battistoniz[:,:,2]*nbe_battistoniz[:,:,0]
+
+ num_battistoniz = np.empty((num_barr.shape[0],num_battistoni.shape[0],4))
+ num_battistoniz[:,:,0] = num_battistoni[:,0]
+ num_battistoniz[:,:,1] = num_zenith_dist_x[:,np.newaxis]
+ num_battistoniz[:,:,2] = num_battistoni[:,1]*num_zenith_dist_y[:,np.newaxis]/(2*np.pi)
+ num_battistoniz[:,:,3] = num_battistoniz[:,:,2]*num_battistoniz[:,:,0]
+
+ nbm_battistoniz = np.empty((nbm_barr.shape[0],nbm_battistoni.shape[0],4))
+ nbm_battistoniz[:,:,0] = nbm_battistoni[:,0]
+ nbm_battistoniz[:,:,1] = nbm_zenith_dist_x[:,np.newaxis]
+ nbm_battistoniz[:,:,2] = nbm_battistoni[:,1]*nbm_zenith_dist_y[:,np.newaxis]/(2*np.pi)
+ nbm_battistoniz[:,:,3] = nbm_battistoniz[:,:,2]*nbm_battistoniz[:,:,0]
+
+ nue_total = np.empty((nue_barr.shape[0],nue_barr.shape[1]+nue_battistoniz.shape[1],4))
+ nue_total[:,:nue_battistoniz.shape[1],:] = nue_battistoniz
+ nue_total[:,nue_battistoniz.shape[1]:,:] = nue_barr[:,:,:4]
+
+ nbe_total = np.empty((nbe_barr.shape[0],nbe_barr.shape[1]+nbe_battistoniz.shape[1],4))
+ nbe_total[:,:nbe_battistoniz.shape[1],:] = nbe_battistoniz
+ nbe_total[:,nbe_battistoniz.shape[1]:,:] = nbe_barr[:,:,:4]
+
+ num_total = np.empty((num_barr.shape[0],num_barr.shape[1]+num_battistoniz.shape[1],4))
+ num_total[:,:num_battistoniz.shape[1],:] = num_battistoniz
+ num_total[:,num_battistoniz.shape[1]:,:] = num_barr[:,:,:4]
+
+ nbm_total = np.empty((nbm_barr.shape[0],nbm_barr.shape[1]+nbm_battistoniz.shape[1],4))
+ nbm_total[:,:nbm_battistoniz.shape[1],:] = nbm_battistoniz
+ nbm_total[:,nbm_battistoniz.shape[1]:,:] = nbm_barr[:,:,:4]
+
+ flux_arrays = {12: nue_total, -12: nbe_total,
+ 14: num_total, -14: nbm_total}
+
+ # Save unoscillated fluxes
+ for p in [12,-12,14,-14]:
+ np.savetxt("fmax20_i0403z_plus_lowe.sno_%s" % pdg_code_to_string(p),flux_arrays[p].reshape((flux_arrays[p].shape[0]*flux_arrays[p].shape[1],-1)),
+ fmt='%10.4f %10.3f %20.8g %20.8g',
+ header="Energy (GeV), Cos(zenith), Flux (m^-2 s^-1 Gev^-1 steradian^-1), Flux (dn/dlnE)")
+
+ osc_prob = {12: nue_osc_prob, -12: nbe_osc_prob,
+ 14: num_osc_prob, -14: nbm_osc_prob}
+
+ def get_flux_interp(nu_type):
+ flux_array = flux_arrays[nu_type]
+ flux = flux_array[:,:,2]
+ e = flux_array[0,:,0]
+ cos_theta = flux_array[:,0,1]
+
+ # Use first order splines here since we need to enforce the fact that
+ # the flux is not negative
+ return RectBivariateSpline(e,cos_theta,flux.T,kx=1,ky=1)
+
+ def get_osc_interp(nu_type1,nu_type2):
+ prob = osc_prob[nu_type1]
+ e = prob[:,0,0]
+ cos_theta = prob[0,:,1]
+
+ if np.sign(nu_type1) != np.sign(nu_type2):
+ raise ValueError("asking for oscillation probability from %s -> %s!" % tuple(map(pdg_code_to_string,(nu_type1,nu_type2))))
+
+ # Use second order splines here since oscillation probabilities are
+ # pretty smooth
+ if abs(nu_type2) == 12:
+ return RectBivariateSpline(e,cos_theta,prob[:,:,2],kx=2,ky=2)
+ elif abs(nu_type2) == 14:
+ return RectBivariateSpline(e,cos_theta,prob[:,:,3],kx=2,ky=2)
+ elif abs(nu_type2) == 16:
+ return RectBivariateSpline(e,cos_theta,prob[:,:,4],kx=2,ky=2)
+
+ def get_oscillated_flux(nu_type1, nu_type2, elo, emid, ehi, zlo, zmid, zhi):
+ """
+ Returns the average flux of neutrinos coming from `nu_type1` neutrinos
+ oscillating to `nu_type2` neutrinos for neutrinos in the bin between
+ energies elo and ehi and with a cosine of the zenith angle between zlo
+ and zhi.
+
+ Note: We also pass the midpoint of the bin for both energy and
+ cos(theta) as `emid` and `zmid`. The reason for this is that at least
+ for the Barr fluxes, the midpoint of the bin was chosen as the midpoint
+ of the log of the left and right edge instead of the actual middle of
+ the bin. Therefore to be consistent when interpolating these fluxes we
+ should use the same value.
+
+ The returned flux has units of 1/(m^2 sec steradian GeV).
+ """
+ f_flux1 = get_flux_interp(nu_type1)
+ f_osc1 = get_osc_interp(nu_type1,nu_type2)
+
+ def f_flux(e,z):
+ # Not sure exactly how scipy deals with boundary issues in
+ # RectBivariateSpline so we just make sure everything is within the
+ # range of the values given in the Barr and Battistoni fluxes
+ if e < 0.0106:
+ e = 0.0106
+ if z < -0.95:
+ z = -0.95
+ if z > 0.95:
+ z = 0.95
+ return f_flux1(e,z)
+
+ def f_osc(e,z):
+ # FIXME: Should remove this
+ if e < 0.02:
+ e = 0.02
+ return f_osc1(e,z)
+
+ # Here we have two options for calculating the oscillated flux:
+ #
+ # 1. We interpolate the value of the flux and then multiply by an
+ # average oscillation probability. This is more correct in the sense
+ # that the original flux values are actually a histogram and so it
+ # makes sense to just sample the flux at the middle of the bin.
+ #
+ # 2. We integrate the product of the flux and the oscillation
+ # probability over the whole bin. This is technically more correct
+ # since it will weight the oscillation probability by the flux, but it
+ # may have issues near the edge of the first and last bin since we have
+ # to extrapolate.
+ #
+ # For now, we do 1 since it is much faster. To do the second you can
+ # just comment the next line.
+ return f_flux(emid,zmid)*f_osc1.integral(elo,ehi,zlo, zhi)/(ehi-elo)/(zhi-zlo)
+ return nquad(lambda e, z: f_flux(e,z)*f_osc(e,z), [(elo,ehi), (zlo, zhi)], opts={'limit':1000,'epsrel':1e-3})[0]/(ehi-elo)/(zhi-zlo)
+
+ data = {nu_type: np.zeros((cos_theta_bins.shape[0]-1,energy_bins.shape[0]-1,4)) for nu_type in [12,-12,14,-14,16,-16]}
+ for nu_type1 in [12,-12,14,-14]:
+ if nu_type1 > 0:
+ nu_type2s = [12,14,16]
+ else:
+ nu_type2s = [-12,-14,-16]
+ for nu_type2 in nu_type2s:
+ print("Calculating neutrino flux for %s -> %s" % tuple(map(pdg_code_to_string,(nu_type1,nu_type2))))
+ for i, (elo, ehi) in enumerate(zip(energy_bins[:-1],energy_bins[1:])):
+ for j, (zlo, zhi) in enumerate(zip(cos_theta_bins[:-1],cos_theta_bins[1:])):
+ print("\r%i/%i" % (i*(len(cos_theta_bins)-1)+j+1,(len(cos_theta_bins)-1)*(len(energy_bins)-1)),end="")
+ sys.stdout.flush()
+ # We'll follow the Barr convention here and take the midpoint in log space
+ emid = np.exp((np.log(elo)+np.log(ehi))/2)
+ zmid = (zlo + zhi)/2
+ data[nu_type2][j,i,0] = emid
+ data[nu_type2][j,i,1] = zmid
+ data[nu_type2][j,i,2] += get_oscillated_flux(nu_type1,nu_type2,elo,emid,ehi,zlo,zmid,zhi)
+ print()
+
+ for nu_type2 in data:
+ for i, (elo, ehi) in enumerate(zip(energy_bins[:-1],energy_bins[1:])):
+ for j, (zlo, zhi) in enumerate(zip(cos_theta_bins[:-1],cos_theta_bins[1:])):
+ data[nu_type2][j,i,3] = data[nu_type2][j,i,2]*data[nu_type2][j,i,0]
+
+ for p in [12,-12,14,-14,16,-16]:
+ np.savetxt("fmax20_i0403z_plus_lowe.sno_%s.osc" % pdg_code_to_string(p),data[p].reshape((data[p].shape[0]*data[p].shape[1],-1)),
+ fmt='%10.4f %10.3f %20.8g %20.8g',
+ header="Energy (GeV), Cos(zenith), Flux (m^-2 s^-1 Gev^-1 steradian^-1), Flux (dn/dlnE), Junk")
diff --git a/utils/calculate-atmospheric-oscillations b/utils/calculate-atmospheric-oscillations
new file mode 100755
index 0000000..356fc1f
--- /dev/null
+++ b/utils/calculate-atmospheric-oscillations
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+"""
+Script to calculate probabilities for atmospheric neutrino oscillations. To run
+it:
+
+ $ ./calculate-atmospheric-oscillations --atmosphere-mode 3 --num-prec 5e-4
+
+which will then produce the following files:
+
+nue_osc_prob.txt: probability for nues to oscillate
+nbe_osc_prob.txt: probability for anti nues to oscillate
+num_osc_prob.txt: probability for numus to oscillate
+nbm_osc_prob.txt: probability for anti numus to oscillate
+
+The format of each text file is the same. It has the following columns:
+
+ 1. Energy of the neutrino in GeV.
+ 2. Cosine of the zenith angle.
+ 3. Probability to oscillate into nue (or antinue)
+ 4. Probability to oscillate into numu (or antinumu)
+ 5. Probability to oscillate into nutau (or antinutau)
+"""
+from __future__ import print_function, division
+import numpy as np
+from time import time
+from NuCraft import *
+
+# depth of the SNO detector (km)
+# currently just converted 6800 feet -> km
+SNO_DEPTH = 2.072
+
+# number of energy bins
+eBins = 1000
+# number of zenith angle bins
+zBins = 100
+# zenith angle bins
+zList = np.arccos(np.linspace(-1,1,zBins))
+# energy range in GeV
+eList = np.logspace(np.log10(0.01), np.log10(10), eBins)
+
+# parameters from PDG 2019
+THETA23 = np.arcsin(np.sqrt(0.512))/np.pi*180.
+THETA13 = np.arcsin(np.sqrt(0.0218))/np.pi*180.
+THETA12 = np.arcsin(np.sqrt(0.307))/np.pi*180.
+DM21 = 7.53e-5
+DM31 = 2.444e-3 + DM21
+
+# Akhemdov is implicitely assuming an electron-to-neutron ratio of 0.5;
+# he is also using the approximation DM31 = DM32;
+# if you want to reproduce his numbers exactly, switch the lines below, and turn
+# atmosphereMode to 0 (no handling of the atmosphere because of )
+# AkhmedovOsci = NuCraft((1., DM21, DM31-DM21), [(1,2,theta12),(1,3,theta13,0),(2,3,theta23)],
+# earthModel=EarthModel("prem", y=(0.5,0.5,0.5))
+# detectorDepth=0., atmHeight=0.)
+AkhmedovOsci = NuCraft((1., DM21, DM31), [(1,2,THETA12),(1,3,THETA13,0),(2,3,THETA23)],detectorDepth=SNO_DEPTH)
+
+def pdg_code_to_string(pdg):
+ if pdg == 12:
+ return "nue"
+ elif pdg == -12:
+ return "nbe"
+ elif pdg == 14:
+ return "num"
+ elif pdg == -14:
+ return "nbm"
+ elif pdg == 16:
+ return "nut"
+ elif pdg == -16:
+ return "nbt"
+ else:
+ raise ValueError("Unknown pdg code %i" % pdg)
+
+if __name__ == '__main__':
+ import argparse
+
+ atmosphereMode = 3 # default: efficiently calculate eight path lenghts per neutrino and take the average
+
+ # This parameter governs the precision with which nuCraft computes the weights; it is the upper
+ # limit for the deviation of the sum of the resulting probabilities from unitarity.
+ # You can verify this by checking the output plot example-standAlone2.png.
+ numPrec = 5e-4
+
+ parser = argparse.ArgumentParser("script to calculate atmospheric neutrino oscillations")
+ parser.add_argument("--atmosphere-mode", type=int, default=atmosphereMode, help="controls the handling of the atmospheric interaction altitude")
+ parser.add_argument("--num-prec", type=float, default=numPrec, help="parameter which governs the precision with which nuCraft computs the weights")
+ args = parser.parse_args()
+
+
+ # 12, -12: NuE, NuEBar
+ # 14, -14: NuMu, NuMuBar
+ # 16, -16: NuTau, NuTauBar
+ for pType in [12,-12,14,-14]:
+ print("Calculating oscillation probabilities for %s..." % pdg_code_to_string(pType))
+
+ # saving the current time to measure the time needed for the execution of the following code
+ t = time()
+
+ ### using lists (arrays, actually)
+ zz, ee = np.meshgrid(zList, eList)
+ zListLong = zz.flatten()
+ eListLong = ee.flatten()
+ tListLong = np.repeat(pType,len(eListLong))
+
+ # actual call to nuCraft for weight calculations:
+ prob = AkhmedovOsci.CalcWeights((tListLong, eListLong, zListLong), numPrec=args.num_prec, atmMode=args.atmosphere_mode)
+
+ prob = np.array(prob)
+
+ save = np.empty((prob.shape[0],5))
+ save[:,0] = eListLong
+ save[:,1] = np.cos(zListLong)
+ save[:,2:] = prob
+
+ print("Calculating the probabilities took %.0f seconds." % (time()-t))
+
+ np.savetxt("%s_osc_prob.txt" % pdg_code_to_string(pType),save,header="Energy (GeV), Cos(zenith), P(nue), P(num), P(nut)")
diff --git a/utils/plot-atmospheric-fluxes b/utils/plot-atmospheric-fluxes
new file mode 100755
index 0000000..fb4861d
--- /dev/null
+++ b/utils/plot-atmospheric-fluxes
@@ -0,0 +1,285 @@
+#!/usr/bin/env python
+"""
+Script for plotting solar fluxes from
+http://www-pnp.physics.ox.ac.uk/~barr/fluxfiles/0403i/index.html.
+"""
+from __future__ import print_function, division
+import numpy as np
+import os
+
+def splitext(path):
+ """
+ Like os.path.splitext() except it returns the full extension if the
+ filename has multiple extensions, for example:
+
+ splitext('foo.tar.gz') -> 'foo', '.tar.gz'
+ """
+ full_root, full_ext = os.path.splitext(path)
+ while True:
+ root, ext = os.path.splitext(full_root)
+ if ext:
+ full_ext = ext + full_ext
+ full_root = root
+ else:
+ break
+
+ return full_root, full_ext
+
+# Taken from https://raw.githubusercontent.com/mwaskom/seaborn/c73055b2a9d9830c6fbbace07127c370389d04dd/seaborn/utils.py
+def despine(fig=None, ax=None, top=True, right=True, left=False,
+ bottom=False, offset=None, trim=False):
+ """Remove the top and right spines from plot(s).
+
+ fig : matplotlib figure, optional
+ Figure to despine all axes of, default uses current figure.
+ ax : matplotlib axes, optional
+ Specific axes object to despine.
+ top, right, left, bottom : boolean, optional
+ If True, remove that spine.
+ offset : int or dict, optional
+ Absolute distance, in points, spines should be moved away
+ from the axes (negative values move spines inward). A single value
+ applies to all spines; a dict can be used to set offset values per
+ side.
+ trim : bool, optional
+ If True, limit spines to the smallest and largest major tick
+ on each non-despined axis.
+
+ Returns
+ -------
+ None
+
+ """
+ # Get references to the axes we want
+ if fig is None and ax is None:
+ axes = plt.gcf().axes
+ elif fig is not None:
+ axes = fig.axes
+ elif ax is not None:
+ axes = [ax]
+
+ for ax_i in axes:
+ for side in ["top", "right", "left", "bottom"]:
+ # Toggle the spine objects
+ is_visible = not locals()[side]
+ ax_i.spines[side].set_visible(is_visible)
+ if offset is not None and is_visible:
+ try:
+ val = offset.get(side, 0)
+ except AttributeError:
+ val = offset
+ _set_spine_position(ax_i.spines[side], ('outward', val))
+
+ # Potentially move the ticks
+ if left and not right:
+ maj_on = any(
+ t.tick1line.get_visible()
+ for t in ax_i.yaxis.majorTicks
+ )
+ min_on = any(
+ t.tick1line.get_visible()
+ for t in ax_i.yaxis.minorTicks
+ )
+ ax_i.yaxis.set_ticks_position("right")
+ for t in ax_i.yaxis.majorTicks:
+ t.tick2line.set_visible(maj_on)
+ for t in ax_i.yaxis.minorTicks:
+ t.tick2line.set_visible(min_on)
+
+ if bottom and not top:
+ maj_on = any(
+ t.tick1line.get_visible()
+ for t in ax_i.xaxis.majorTicks
+ )
+ min_on = any(
+ t.tick1line.get_visible()
+ for t in ax_i.xaxis.minorTicks
+ )
+ ax_i.xaxis.set_ticks_position("top")
+ for t in ax_i.xaxis.majorTicks:
+ t.tick2line.set_visible(maj_on)
+ for t in ax_i.xaxis.minorTicks:
+ t.tick2line.set_visible(min_on)
+
+ if trim:
+ # clip off the parts of the spines that extend past major ticks
+ xticks = ax_i.get_xticks()
+ if xticks.size:
+ firsttick = np.compress(xticks >= min(ax_i.get_xlim()),
+ xticks)[0]
+ lasttick = np.compress(xticks <= max(ax_i.get_xlim()),
+ xticks)[-1]
+ ax_i.spines['bottom'].set_bounds(firsttick, lasttick)
+ ax_i.spines['top'].set_bounds(firsttick, lasttick)
+ newticks = xticks.compress(xticks <= lasttick)
+ newticks = newticks.compress(newticks >= firsttick)
+ ax_i.set_xticks(newticks)
+
+ xticks_minor = [t for t in ax_i.get_xticks(minor=True) if firsttick < t < lasttick]
+ ax_i.set_xticks(xticks_minor,minor=True)
+
+ yticks = ax_i.get_yticks()
+ if yticks.size:
+ firsttick = np.compress(yticks >= min(ax_i.get_ylim()),
+ yticks)[0]
+ lasttick = np.compress(yticks <= max(ax_i.get_ylim()),
+ yticks)[-1]
+ ax_i.spines['left'].set_bounds(firsttick, lasttick)
+ ax_i.spines['right'].set_bounds(firsttick, lasttick)
+ newticks = yticks.compress(yticks <= lasttick)
+ newticks = newticks.compress(newticks >= firsttick)
+ ax_i.set_yticks(newticks)
+
+ yticks_minor = [t for t in ax_i.get_yticks(minor=True) if firsttick < t < lasttick]
+ ax_i.set_yticks(yticks_minor,minor=True)
+
+# Data is in the form: Fluxes in bins of neutrino energy (equally spaced bins
+# in logE with 10 bins per decade with the low edge of the first bin at 100
+# MeV) and zenith angle (20 bins equally spaced in cos(zenith) with bin width
+# 0.1), integrated over azimuth. Note logE means log to base e. Fluxes below 10
+# GeV are from the 3D calculation.
+
+# The files all have the same format. After the initial comment lines (starting
+# with a # character), the files contain one line per bin. No smoothoing
+# between bins has been done. The columns are:
+#
+# 1. Energy at centre of bin in GeV
+# 2. Zenith files: Cos(zenith) at centre of bin
+# Azimuth files: Azimuth at centre of bin (degrees)
+# 3. Flux in dN/dlogE in /m**2/steradian/sec
+# 4. MC Statistical error on the flux
+# 5. Number of unweighted events entering the bin (not too useful)
+
+if __name__ == '__main__':
+ import argparse
+ import matplotlib
+ import glob
+
+ parser = argparse.ArgumentParser("plot solar fluxes")
+ parser.add_argument("filenames", nargs='+', help="filenames of flux files")
+ parser.add_argument("--save", action="store_true", default=False, help="save plots")
+ args = parser.parse_args()
+
+ if args.save:
+ # default \textwidth for a fullpage article in Latex is 16.50764 cm.
+ # You can figure this out by compiling the following TeX document:
+ #
+ # \documentclass{article}
+ # \usepackage{fullpage}
+ # \usepackage{layouts}
+ # \begin{document}
+ # textwidth in cm: \printinunitsof{cm}\prntlen{\textwidth}
+ # \end{document}
+
+ width = 16.50764
+ width /= 2.54 # cm -> inches
+ # According to this page:
+ # http://www-personal.umich.edu/~jpboyd/eng403_chap2_tuftegospel.pdf,
+ # Tufte suggests an aspect ratio of 1.5 - 1.6.
+ height = width/1.5
+ FIGSIZE = (width,height)
+
+ import matplotlib.pyplot as plt
+
+ font = {'family':'serif', 'serif': ['computer modern roman']}
+ plt.rc('font',**font)
+
+ plt.rc('text', usetex=True)
+ else:
+ # 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")
+
+ import matplotlib.pyplot as plt
+
+ # Default figure size. Currently set to my monitor width and height so that
+ # things are properly formatted
+ FIGSIZE = (13.78,7.48)
+
+ font = {'family':'serif', 'serif': ['computer modern roman']}
+ plt.rc('font',**font)
+
+ # Make the defalt font bigger
+ plt.rc('font', size=22)
+
+ plt.rc('text', usetex=True)
+
+ fig = plt.figure(figsize=FIGSIZE)
+
+ colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
+ linestyles = ['-','--']
+
+ def key(filename):
+ head, tail = os.path.split(filename)
+
+ k = 0
+ if tail.startswith('fmax'):
+ k += 1
+ if 'nue' in tail:
+ k += 10
+ elif 'nbe' in tail:
+ k += 20
+ elif 'num' in tail:
+ k += 30
+ elif 'nbm' in tail:
+ k += 40
+ elif 'nut' in tail:
+ k += 50
+ elif 'nbt' in tail:
+ k += 60
+
+ return k
+
+ for filename in sorted(args.filenames,key=key):
+ head, tail = os.path.split(filename)
+
+ print(filename)
+
+ data = np.genfromtxt(filename)
+
+ shape1 = len(np.unique(data[:,0]))
+
+ x = data[:,0].reshape((-1,shape1))
+ y = data[:,1].reshape((-1,shape1))
+ z = data[:,2].reshape((-1,shape1))
+
+ # Convert to MeV
+ x *= 1000.0
+ z /= 1000.0
+
+ zbins = np.linspace(-1,1,21)
+ dz = zbins[1] - zbins[0]
+
+ x = x[0]
+ # Integrate over cos(theta) and multiply by 2*pi to convert 3D flux to
+ # a total flux
+ y = np.sum(z*dz,axis=0)*2*np.pi
+
+ if 'sno_nue' in tail:
+ plt.plot(x,y,color=colors[0],linestyle=linestyles[0],label=r'$\nu_e$')
+ elif 'sno_nbe' in tail:
+ plt.plot(x,y,color=colors[0],linestyle=linestyles[1],label=r'$\overline{\nu}_e$')
+ elif 'sno_num' in tail:
+ plt.plot(x,y,color=colors[1],linestyle=linestyles[0],label=r'$\nu_\mu$')
+ elif 'sno_nbm' in tail:
+ plt.plot(x,y,color=colors[1],linestyle=linestyles[1],label=r'$\overline{\nu}_\mu$')
+ elif 'sno_nut' in tail:
+ plt.plot(x,y,color=colors[2],linestyle=linestyles[0],label=r'$\nu_\tau$')
+ elif 'sno_nbt' in tail:
+ plt.plot(x,y,color=colors[2],linestyle=linestyles[1],label=r'$\overline{\nu}_\tau$')
+
+ plt.gca().set_xscale("log")
+ plt.gca().set_yscale("log")
+ despine(fig,trim=True)
+ plt.xlabel("$E$ (MeV)")
+ plt.ylabel(r"$\mathrm{d}\Phi/\mathrm{d}E$ (1/$\mathrm{m}^2$/sec/MeV)")
+ plt.legend()
+ plt.tight_layout()
+
+ if args.save:
+ plt.savefig("irc01_atmospheric_flux.pdf")
+ plt.savefig("irc01_atmospheric_flux.eps")
+
+ plt.show()
diff --git a/utils/plot-atmospheric-oscillations b/utils/plot-atmospheric-oscillations
new file mode 100755
index 0000000..c24a776
--- /dev/null
+++ b/utils/plot-atmospheric-oscillations
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+"""
+Script to plot the probabilities for atmospheric neutrino oscillations. To run
+it:
+
+ $ ./plot-atmospheric-oscillations nue_osc_prob.txt
+"""
+from __future__ import print_function, division
+import numpy as np
+
+if __name__ == '__main__':
+ import argparse
+ from os.path import split, splitext
+
+ parser = argparse.ArgumentParser("script to plot atmospheric oscillations")
+ parser.add_argument("filenames", nargs='+', help="oscillation probability filenames")
+ parser.add_argument("--save", action="store_true", default=False, help="save plots")
+ args = parser.parse_args()
+
+ if args.save:
+ # default \textwidth for a fullpage article in Latex is 16.50764 cm.
+ # You can figure this out by compiling the following TeX document:
+ #
+ # \documentclass{article}
+ # \usepackage{fullpage}
+ # \usepackage{layouts}
+ # \begin{document}
+ # textwidth in cm: \printinunitsof{cm}\prntlen{\textwidth}
+ # \end{document}
+
+ width = 16.50764
+ width /= 2.54 # cm -> inches
+ # According to this page:
+ # http://www-personal.umich.edu/~jpboyd/eng403_chap2_tuftegospel.pdf,
+ # Tufte suggests an aspect ratio of 1.5 - 1.6.
+ height = width/1.5
+ FIGSIZE = (width,height)
+
+ import matplotlib.pyplot as plt
+
+ font = {'family':'serif', 'serif': ['computer modern roman']}
+ plt.rc('font',**font)
+
+ plt.rc('text', usetex=True)
+ else:
+ # 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")
+
+ import matplotlib.pyplot as plt
+
+ # Default figure size. Currently set to my monitor width and height so that
+ # things are properly formatted
+ FIGSIZE = (13.78,7.48)
+
+ # Make the defalt font bigger
+ plt.rc('font', size=22)
+
+ for filename in args.filenames:
+ head, tail = split(filename)
+ root, ext = splitext(tail)
+
+ e, z, pnue, pnum, pnut = np.genfromtxt(filename).T
+
+ shape0 = len(np.unique(e))
+
+ ee = e.reshape((shape0,-1))
+ zz = z.reshape((shape0,-1))
+ pnue = pnue.reshape((shape0,-1))
+ pnum = pnum.reshape((shape0,-1))
+ pnut = pnut.reshape((shape0,-1))
+
+ levels = np.linspace(0,1,101)
+
+ plt.figure(figsize=FIGSIZE)
+ plt.contourf(ee,zz,pnue,levels=levels)
+ plt.gca().set_xscale('log')
+ plt.xlabel("Energy (GeV)")
+ plt.ylabel("Cos(Zenith)")
+ plt.colorbar()
+ plt.tight_layout()
+ if args.save:
+ plt.savefig("%s_nue.pdf" % root)
+ plt.savefig("%s_nue.eps" % root)
+ else:
+ plt.title(r"Probability to oscillate to $\nu_e$")
+ plt.figure(figsize=FIGSIZE)
+ plt.contourf(ee,zz,pnum,levels=levels)
+ plt.gca().set_xscale('log')
+ plt.xlabel("Energy (GeV)")
+ plt.ylabel("Cos(Zenith)")
+ plt.colorbar()
+ plt.tight_layout()
+ if args.save:
+ plt.savefig("%s_num.pdf" % root)
+ plt.savefig("%s_num.eps" % root)
+ else:
+ plt.title(r"Probability to oscillate to $\nu_\mu$")
+ plt.figure(figsize=FIGSIZE)
+ plt.contourf(ee,zz,pnut,levels=levels)
+ plt.gca().set_xscale('log')
+ plt.xlabel("Energy (GeV)")
+ plt.ylabel("Cos(Zenith)")
+ plt.colorbar()
+ plt.tight_layout()
+ if args.save:
+ plt.savefig("%s_nut.pdf" % root)
+ plt.savefig("%s_nut.eps" % root)
+ else:
+ plt.title(r"Probability to oscillate to $\nu_\tau$")
+
+ plt.show()