def __init__(self, costh, pmodel=(pm.HillasGaisser2012, 'H3a'), hadr='SIBYLL2.3c', barr_mods=(), depth=1950 * Units.m, density=('CORSIKA', ('SouthPole', 'June'))): """Initializes the nuVeto object for a particular costheta, CR Flux, hadronic model, barr parameters, and depth Note: A separate MCEq instance needs to be created for each combination of __init__'s arguments. To access pmodel and hadr, use mceq.pm_params and mceq.yields_params Args: costh (float): Cos(theta), the cosine of the neutrino zenith at the detector pmodel (tuple(CR model class, arguments)): CR Flux hadr (str): hadronic interaction model barr_mods: barr parameters depth (float): the depth at which the veto probability is computed below the ice """ self.costh = costh self.pmodel = pmodel self.geom = Geometry(depth) theta = np.degrees(np.arccos(self.geom.cos_theta_eff(self.costh))) MCEq.core.dbg = 0 MCEq.kernels.dbg = 0 MCEq.density_profiles.dbg = 0 MCEq.data.dbg = 0 self.mceq = MCEqRun( # provide the string of the interaction model interaction_model=hadr, # atmospheric density model density_model=density, # primary cosmic ray flux model # support a tuple (primary model class (not instance!), arguments) primary_model=pmodel, # zenith angle \theta in degrees, measured positively from vertical direction theta_deg=theta, enable_muon_energy_loss=False, **mceq_config_without(['enable_muon_energy_loss', 'density_model'])) for barr_mod in barr_mods: # Modify proton-air -> mod[0] self.mceq.set_mod_pprod(2212, BARR[barr_mod[0]].pdg, barr_unc, barr_mod) # Populate the modifications to the matrices by re-filling the interaction matrix self.mceq._init_default_matrices(skip_D_matrix=True) X_vec = np.logspace(np.log10(2e-3), np.log10(self.mceq.density_model.max_X), 12) self.dX_vec = np.diff(X_vec) self.X_vec = 10**centers(np.log10(X_vec))
def plot_prpl(interp_pkl, include_mean=False, include_cbar=True): depth = 1950 * Units.m prplfn = pickle.load(open(interp_pkl, 'rb'), encoding='latin1') emui_edges = np.logspace(2, 8, 101) l_ice_edges = np.linspace(1e3, 4e4, 101) emui = centers(emui_edges) l_ice = centers(l_ice_edges) xx, yy = np.meshgrid(emui, l_ice) prpls = prplfn(list(zip(xx.flatten(), yy.flatten()))) plt.figure() plt.pcolormesh(emui_edges, l_ice_edges / 1e3, prpls.reshape(xx.shape), cmap='magma') if include_cbar: plt.colorbar() if include_mean: small_ice = l_ice[l_ice < 2.7e4] plt.plot( extsv.minimum_muon_energy(small_ice), small_ice / 1e3, 'w--', label= r'$l_{\rm ice,\,median} (E_\mu^{\rm i}, E_\mu^{\rm th} = 1\,{\rm TeV})$' ) leg = plt.legend(frameon=False, prop={'weight': 'bold'}, loc='upper left') for text in leg.get_texts(): plt.setp(text, color='w', fontsize='medium') plt.xlabel(r'$E_\mu^{\rm i}$ [GeV]') plt.ylabel(r'$l_{\rm ice}$ [km]') plt.locator_params(axis='y', nbins=8) # # plt.yscale('log') # # plt.gca().yaxis.set_major_formatter(ScalarFormatter()) # plt.ticklabel_format(style='plain', axis='y') plt.gca().minorticks_off() plt.ylim(depth / Units.km, 40) # right y-axis with angles axr = plt.gca().twinx() axr.grid(False) geom = Geometry(depth) costhetas = geom.overburden_to_cos_theta(np.arange(10, 41, 10) * Units.km) axr.set_ylim(depth / Units.km, 40) axr.set_yticks(geom.overburden(costhetas) / 1e3) axr.set_yticklabels(np.round(costhetas, 2)) axr.set_ylabel(r'$\cos \theta_z$', rotation=-90) axr.set_xscale('log') axr.set_xlim(1e2, 1e8) axr.minorticks_off() xlocmaj = LogLocator(base=10, numticks=12) axr.get_xaxis().set_major_locator(xlocmaj) return emui_edges, l_ice_edges, prpls.reshape(xx.shape)
def test_overburden(): geom = Geometry(1950 * Units.m) cosths = np.linspace(-1, 1, 100) assert np.all(np.diff(geom.overburden(cosths)) < 0) center = Geometry(geom.r_E) assert np.all(center.overburden(cosths) == geom.r_E / Units.m)
def test_costh_effective(): geom = Geometry(1950 * Units.m) cosths = np.linspace(-1, 1, 100) assert np.all(geom.cos_theta_eff(cosths) >= cosths) center = Geometry(geom.r_E) assert np.all(center.cos_theta_eff(cosths) == np.ones(100))
class nuVeto(object): """Class for computing the neutrino passing fraction i.e. (1-(Veto probability))""" def __init__(self, costh, pmodel=(pm.HillasGaisser2012, 'H3a'), hadr='SIBYLL2.3c', barr_mods=(), depth=1950 * Units.m, density=('CORSIKA', ('SouthPole', 'June'))): """Initializes the nuVeto object for a particular costheta, CR Flux, hadronic model, barr parameters, and depth Note: A separate MCEq instance needs to be created for each combination of __init__'s arguments. To access pmodel and hadr, use mceq.pm_params and mceq.yields_params Args: costh (float): Cos(theta), the cosine of the neutrino zenith at the detector pmodel (tuple(CR model class, arguments)): CR Flux hadr (str): hadronic interaction model barr_mods: barr parameters depth (float): the depth at which the veto probability is computed below the ice """ self.costh = costh self.pmodel = pmodel self.geom = Geometry(depth) theta = np.degrees(np.arccos(self.geom.cos_theta_eff(self.costh))) MCEq.core.dbg = 0 MCEq.kernels.dbg = 0 MCEq.density_profiles.dbg = 0 MCEq.data.dbg = 0 self.mceq = MCEqRun( # provide the string of the interaction model interaction_model=hadr, # atmospheric density model density_model=density, # primary cosmic ray flux model # support a tuple (primary model class (not instance!), arguments) primary_model=pmodel, # zenith angle \theta in degrees, measured positively from vertical direction theta_deg=theta, enable_muon_energy_loss=False, **mceq_config_without(['enable_muon_energy_loss', 'density_model'])) for barr_mod in barr_mods: # Modify proton-air -> mod[0] self.mceq.set_mod_pprod(2212, BARR[barr_mod[0]].pdg, barr_unc, barr_mod) # Populate the modifications to the matrices by re-filling the interaction matrix self.mceq._init_default_matrices(skip_D_matrix=True) X_vec = np.logspace(np.log10(2e-3), np.log10(self.mceq.density_model.max_X), 12) self.dX_vec = np.diff(X_vec) self.X_vec = 10**centers(np.log10(X_vec)) @staticmethod def categ_to_mothers(categ, daughter): """Get the parents for this category""" rcharge = '-' if 'anti' in daughter else '+' lcharge = '+' if 'anti' in daughter else '-' rbar = '-bar' if 'anti' in daughter else '' #lbar = '' if 'anti' in daughter else '-bar' if categ == 'conv': mothers = ['pi' + rcharge, 'K' + rcharge, 'K0L'] if 'nutau' in daughter: mothers = [] elif 'nue' in daughter: mothers.extend(['K0S', 'mu' + rcharge]) elif 'numu' in daughter: mothers.extend(['mu' + lcharge]) elif categ == 'pr': if 'nutau' in daughter: mothers = ['D' + rcharge, 'Ds' + rcharge] else: mothers = ['D' + rcharge, 'Ds' + rcharge, 'D0' + rbar] #, 'Lambda0'+lbar]#, 'LambdaC+'+bar] elif categ == 'total': mothers = nuVeto.categ_to_mothers( 'conv', daughter) + nuVeto.categ_to_mothers('pr', daughter) else: mothers = [ categ, ] return mothers @staticmethod def esamp(enu, accuracy): """ returns the sampling of parent energies for a given enu """ # TODO: replace 1e8 with MMC-prpl interpolated bounds return np.logspace(np.log10(enu), np.log10(enu + 1e8), 1000 * accuracy) @staticmethod def projectiles(): """Get allowed pimaries""" pdg_ids = config['adv_set']['allowed_projectiles'] namer = ParticleProperties.modtab.pdg2modname allowed = [] for pdg_id in pdg_ids: allowed.append(namer[pdg_id]) try: allowed.append(namer[-pdg_id]) except KeyError: continue return allowed @staticmethod def nbody(fpath, esamp, enu, fn, l_ice): with np.load(fpath) as dfile: xmus = centers(dfile['xedges']) xnus = np.concatenate([xmus, [1]]) vals = np.nan_to_num(dfile['histograms']) ddec = interpolate.RegularGridInterpolator((xnus, xmus), vals, bounds_error=False, fill_value=None) emu_mat = xmus[:, None] * esamp[None, :] * Units.GeV pmu_mat = ddec(np.stack(np.meshgrid(enu / esamp, xmus), axis=-1)) reaching = 1 - np.sum(pmu_mat * fn.prpl( np.stack([emu_mat, np.ones(emu_mat.shape) * l_ice], axis=-1)), axis=0) reaching[reaching < 0.] = 0. return reaching @staticmethod @lru_cache(2**12) def psib(l_ice, mother, enu, accuracy, prpl): """ returns the suppression factor due to the sibling muon """ esamp = nuVeto.esamp(enu, accuracy) fn = MuonProb(prpl) if mother in ['D0', 'D0-bar']: reaching = nuVeto.nbody( resource_filename('nuVeto', 'data/decay_distributions/D0_numu.npz'), esamp, enu, fn, l_ice) elif mother in ['D+', 'D-']: reaching = nuVeto.nbody( resource_filename('nuVeto', 'data/decay_distributions/D+_numu.npz'), esamp, enu, fn, l_ice) elif mother in ['Ds+', 'Ds-']: reaching = nuVeto.nbody( resource_filename('nuVeto', 'data/decay_distributions/Ds_numu.npz'), esamp, enu, fn, l_ice) elif mother == 'K0L': reaching = nuVeto.nbody( resource_filename('nuVeto', 'data/decay_distributions/K0L_numu.npz'), esamp, enu, fn, l_ice) else: # Assuming muon energy is E_parent - E_nu reaching = 1. - fn.prpl( zip((esamp - enu) * Units.GeV, [l_ice] * len(esamp))) return reaching @lru_cache(maxsize=2**12) def get_dNdEE(self, mother, daughter): """Differential parent-->neutrino (mother--daughter) yield""" ihijo = 20 e_grid = self.mceq.e_grid delta = self.mceq.e_widths x_range = e_grid[ihijo] / e_grid rr = ParticleProperties.rr(mother, daughter) dNdEE_edge = ParticleProperties.br_2body(mother, daughter) / (1 - rr) dN_mat = self.mceq.decays.get_d_matrix( ParticleProperties.pdg_id[mother], ParticleProperties.pdg_id[daughter]) dNdEE = dN_mat[ihijo] * e_grid / delta logx = np.log10(x_range) logx_width = -np.diff(logx)[0] good = (logx + logx_width / 2 < np.log10(1 - rr)) & (x_range >= 5.e-2) x_low = x_range[x_range < 5e-2] dNdEE_low = np.array([dNdEE[good][-1]] * x_low.size) dNdEE_interp = lambda x_: interpolate.pchip( np.concatenate([[1 - rr], x_range[good], x_low])[::-1], np.concatenate([[dNdEE_edge], dNdEE[good], dNdEE_low])[::-1], extrapolate=True)(x_) * np.heaviside(1 - rr - x_, 1) return x_range, dNdEE, dNdEE_interp @lru_cache(maxsize=2**12) def grid_sol(self, ecr=None, particle=None): """MCEq grid solution for \\frac{dN_{CR,p}}_{dE_p}""" if ecr is not None: self.mceq.set_single_primary_particle(ecr, particle) else: self.mceq.set_primary_model(*self.pmodel) self.mceq.solve(int_grid=self.X_vec, grid_var="X") return self.mceq.grid_sol @lru_cache(maxsize=2**12) def nmu(self, ecr, particle, prpl='ice_allm97_step_1'): """Poisson probability of getting no muons""" grid_sol = self.grid_sol(ecr, particle) l_ice = self.geom.overburden(self.costh) mu = self.get_solution('mu-', grid_sol) + self.get_solution( 'mu+', grid_sol) fn = MuonProb(prpl) coords = zip(self.mceq.e_grid * Units.GeV, [l_ice] * len(self.mceq.e_grid)) return np.trapz(mu * fn.prpl(coords), self.mceq.e_grid) @lru_cache(maxsize=2**12) def get_rescale_phi(self, mother, ecr=None, particle=None): """Flux of the mother at all heights""" grid_sol = self.grid_sol( ecr, particle ) # MCEq solution (fluxes tabulated as a function of height) dX = self.dX_vec * Units.gr / Units.cm**2 rho = self.mceq.density_model.X2rho( self.X_vec) * Units.gr / Units.cm**3 inv_decay_length_array = ( ParticleProperties.mass_dict[mother] / (self.mceq.e_grid[:, None] * Units.GeV)) / ( ParticleProperties.lifetime_dict[mother] * rho[None, :]) rescale_phi = dX[None, :] * inv_decay_length_array * self.get_solution( mother, grid_sol, grid_idx=False).T return rescale_phi def get_integrand(self, categ, daughter, enu, accuracy, prpl, ecr=None, particle=None): """flux*yield""" esamp = self.esamp(enu, accuracy) mothers = self.categ_to_mothers(categ, daughter) nums = np.zeros((len(esamp), len(self.X_vec))) dens = np.zeros((len(esamp), len(self.X_vec))) for mother in mothers: dNdEE = self.get_dNdEE(mother, daughter)[-1] rescale_phi = self.get_rescale_phi(mother, ecr, particle) # DEBUG # from matplotlib import pyplot as plt # plt.plot(np.log(self.mceq.e_grid[rescale_phi[:,0]>0]), # np.log(rescale_phi[:,0][rescale_phi[:,0]>0])) # rescale_phi = np.array([interpolate.interp1d(self.mceq.e_grid, rescale_phi[:,i], kind='quadratic', bounds_error=False, fill_value=0)(esamp) for i in xrange(rescale_phi.shape[1])]).T ### # TODO: optimize to only run when esamp[0] is non-zero rescale_phi = np.exp( np.array([ interpolate.interp1d( np.log(self.mceq.e_grid[rescale_phi[:, i] > 0]), np.log(rescale_phi[:, i][rescale_phi[:, i] > 0]), kind='quadratic', bounds_error=False, fill_value=-np.inf)(np.log(esamp)) for i in xrange(rescale_phi.shape[1]) ])).T # DEBUG # print rescale_phi.min(), rescale_phi.max() # print np.log(esamp) # plt.plot(np.log(esamp), # np.log(rescale_phi[:,0]), label='intp') # plt.legend() # import pdb # pdb.set_trace() ### if 'numu' in daughter: # muon accompanies numu only pnmsib = self.psib(self.geom.overburden(self.costh), mother, enu, accuracy, prpl) else: pnmsib = np.ones(len(esamp)) dnde = dNdEE(enu / esamp) / esamp nums += (dnde * pnmsib)[:, None] * rescale_phi dens += (dnde)[:, None] * rescale_phi return nums, dens def get_solution(self, particle_name, grid_sol, mag=0., grid_idx=None): """Retrieves solution of the calculation on the energy grid. Args: particle_name (str): The name of the particle such, e.g. ``total_mu+`` for the total flux spectrum of positive muons or ``pr_antinumu`` for the flux spectrum of prompt anti muon neutrinos mag (float, optional): 'magnification factor': the solution is multiplied by ``sol`` :math:`= \\Phi \\cdot E^{mag}` grid_idx (int, optional): if the integrator has been configured to save intermediate solutions on a depth grid, then ``grid_idx`` specifies the index of the depth grid for which the solution is retrieved. If not specified the flux at the surface is returned integrate (bool, optional): return averge particle number instead of flux (multiply by bin width) Returns: (numpy.array): flux of particles on energy grid :attr:`e_grid` """ # MCEq index conversion ref = self.mceq.pname2pref p_pdg = ParticleProperties.pdg_id[particle_name] reduce_res = True if grid_idx is None: # Surface only case sol = np.array([grid_sol[-1]]) xv = np.array([self.X_vec[-1]]) elif isinstance(grid_idx, bool) and not grid_idx: # Whole solution case sol = np.asarray(grid_sol) xv = np.asarray(self.X_vec) reduce_res = False elif grid_idx >= len(self.mceq.grid_sol): # Surface only case sol = np.array([grid_sol[-1]]) xv = np.array([self.X_vec[-1]]) else: # Particular height case sol = np.array([grid_sol[grid_idx]]) xv = np.array([self.X_vec[grid_idx]]) # MCEq solution for particle direct = sol[:, ref[particle_name].lidx():ref[particle_name].uidx()] res = np.zeros(direct.shape) rho_air = 1. / self.mceq.density_model.r_X2rho(xv) # meson decay length decayl = ((self.mceq.e_grid * Units.GeV) / ParticleProperties.mass_dict[particle_name] * ParticleProperties.lifetime_dict[particle_name] / Units.cm) # number of targets per cm2 ndens = rho_air * Units.Na / Units.mol_air for prim in self.projectiles(): prim_flux = sol[:, ref[prim].lidx():ref[prim].uidx()] prim_xs = self.mceq.cs.get_cs(ParticleProperties.pdg_id[prim]) try: int_yields = self.mceq.y.get_y_matrix( ParticleProperties.pdg_id[prim], p_pdg) res += np.sum(int_yields[None, :, :] * prim_flux[:, None, :] * prim_xs[None, None, :] * ndens[:, None, None], axis=2) except KeyError as e: continue res *= decayl[None, :] # combine with direct res[direct != 0] = direct[direct != 0] if particle_name[:-1] == 'mu': for _ in [ 'k_' + particle_name, 'pi_' + particle_name, 'pr_' + particle_name ]: res += sol[:, ref[_].lidx():ref[_].uidx()] res *= self.mceq.e_grid[None, :]**mag if reduce_res: res = res[0] return res def get_fluxes(self, enu, kind='conv_numu', accuracy=3.5, prpl='ice_allm97_step_1', corr_only=False): """Returns the flux and passing fraction for a particular neutrino energy, flux, and p_light """ # prpl = probability of reaching * probability of light # prpl -> None ==> median for muon reaching categ, daughter = kind.split('_') esamp = self.esamp(enu, accuracy) # Correlated only (no need for the unified calculation here) [really just for testing] passed = 0 total = 0 if corr_only: # sum performs the dX integral nums, dens = self.get_integrand(categ, daughter, enu, accuracy, prpl) num = np.sum(nums, axis=1) den = np.sum(dens, axis=1) passed = integrate.trapz(num, esamp) total = integrate.trapz(den, esamp) return passed, total pmodel = self.pmodel[0](self.pmodel[1]) #loop over primary particles for particle in pmodel.nucleus_ids: # A continuous input energy range is allowed between # :math:`50*A~ \\text{GeV} < E_\\text{nucleus} < 10^{10}*A \\text{GeV}`. # ecrs --> Energy of cosmic ray primaries # amu --> atomic mass of primary # evaluation points in E_CR ecrs = amu(particle) * np.logspace(2, 10, 10 * accuracy) # pnm --> probability of no muon (just a poisson probability) nmu = [self.nmu(ecr, particle, prpl) for ecr in ecrs] # nmufn --> fine grid interpolation of pnm nmufn = interpolate.interp1d(ecrs, nmu, kind='linear', assume_sorted=True, bounds_error=False, fill_value=(0, np.nan)) # nums --> numerator nums = [] # dens --> denominator dens = [] # istart --> integration starting point, the lowest energy index for the integral istart = max(0, np.argmax(ecrs > enu) - 1) for ecr in ecrs[istart:]: # integral in primary energy (E_CR) # cr_flux --> cosmic ray flux # phim2 --> units of flux * m^2 (look it up in the units) cr_flux = pmodel.nucleus_flux(particle, ecr.item()) * Units.phim2 # poisson exp(-Nmu) [last term in eq 12] pnmarr = np.exp(-nmufn(ecr - esamp)) num_ecr = 0 # single entry in nums den_ecr = 0 # single entry in dens # dEp # integral in Ep nums_ecr, dens_ecr = self.get_integrand( categ, daughter, enu, accuracy, prpl, ecr, particle) num_ecr = integrate.trapz( np.sum(nums_ecr, axis=1) * pnmarr, esamp) den_ecr = integrate.trapz(np.sum(dens_ecr, axis=1), esamp) nums.append(num_ecr * cr_flux / Units.phicm2) dens.append(den_ecr * cr_flux / Units.phicm2) # dEcr passed += integrate.trapz(nums, ecrs[istart:]) total += integrate.trapz(dens, ecrs[istart:]) return passed, total