def get_H_vac(mix_nubar, mix_nubar_conj_transp, dm_vac_vac, H_vac): """ Calculate vacuum Hamiltonian in flavor basis for neutrino or antineutrino Parameters: ----------- mix_nubar : complex 2d array Mixing matrix, already conjugated if antineutrino mix_nubar_conj_transp : conjugate 2d array Conjugate transpose of mix_nubar dm_vac_vac : 2d array Matrix of mass splittings H_vac : complex 2d array Hamiltonian in vacuum, without the 1/2E term Notes ------ The Hailtonian does not contain the energy dependent factor of 1/(2 * E), as it will be added later """ dm_vac_diag = cuda.local.array(shape=(3, 3), dtype=ctype) tmp = cuda.local.array(shape=(3, 3), dtype=ctype) clear_matrix(dm_vac_diag) dm_vac_diag[1, 1] = dm_vac_vac[1, 0] + 0j dm_vac_diag[2, 2] = dm_vac_vac[2, 0] + 0j matrix_dot_matrix(dm_vac_diag, mix_nubar_conj_transp, tmp) matrix_dot_matrix(mix_nubar, tmp, H_vac)
def get_H_vac(mix_nubar, mix_nubar_conj_transp, dm_vac_vac, H_vac): ''' Calculate vacuum Hamiltonian in flavor basis for neutrino or antineutrino Parameters: ----------- mix_nubar : complex 2d-array Mixing matrix (comjugate for anti-neutrinos) mix_nubar_conj_transp : comjugate 2d-array conjugate transpose of mixing matrix dm_vac_vac: 2d-array Matrix of mass splittings H_vac: complex 2d-array (empty) Hamiltonian in vacuum, modulo a factor 2 * energy Notes ------ The Hailtonian does not contain the energy dependent factor of 1/(2 * E), as it will be added later ''' dm_vac_diag = cuda.local.array(shape=(3, 3), dtype=ctype) tmp = cuda.local.array(shape=(3, 3), dtype=ctype) clear_matrix(dm_vac_diag) dm_vac_diag[1, 1] = dm_vac_vac[1, 0] + 0j dm_vac_diag[2, 2] = dm_vac_vac[2, 0] + 0j matrix_dot_matrix(dm_vac_diag, mix_nubar_conj_transp, tmp) matrix_dot_matrix(mix_nubar, tmp, H_vac)
def get_transition_matrix_massbasis( baseline, energy, dm_mat_vac, dm_mat_mat, H_mat_mass_eigenstate_basis, transition_matrix, ): """ Calculate the transition amplitude matrix Parameters ---------- baseline : float Baseline traversed, km energy : float Neutrino energy, GeV dm_mat_vac : complex 2d array dm_mat_mat : complex 2d array H_mat_mass_eigenstate_basis : complex 2d array transition_matrix : complex 2d array (empty) Transition matrix in mass eigenstate basis Notes ----- - corrsponds to matrix A (equation 10) in original Barger paper - take into account generic potential matrix (=Hamiltonian) """ product = cuda.local.array(shape=(3, 3, 3), dtype=ctype) clear_matrix(transition_matrix) get_product(energy, dm_mat_vac, dm_mat_mat, H_mat_mass_eigenstate_basis, product) # (1/2)*(1/(h_bar*c)) in units of GeV/(eV^2 km) hbar_c_factor = 2.534 for k in range(3): arg = -dm_mat_vac[k, 0] * (baseline / energy) * hbar_c_factor c = cmath.exp(arg * 1.0j) for i in range(3): for j in range(3): transition_matrix[i, j] += c * product[i, j, k]
def get_H_mat(rho, mat_pot, nubar, H_mat): """ Calculate matter Hamiltonian in flavor basis Parameters: ----------- rho : real float Electron number density (in moles/cm^3) (numerically, this is just the product of electron fraction and mass density in g/cm^3, since the number of grams per cm^3 corresponds to the number of moles of nucleons per cm^3) mat_pot : complex 2d array Generalised matter potential matrix without "a" factor (will be multiplied with "a" factor); set to diag([1, 0, 0]) for only standard oscillations nubar : int +1 for neutrinos, -1 for antineutrinos H_mat : complex 2d array (empty) matter hamiltonian Notes ----- In the following, `a` is just the standard effective matter potential induced by charged-current weak interactions with electrons """ # 2*sqrt(2)*Gfermi in (eV^2 cm^3)/(mole GeV) tworttwoGf = 1.52588e-4 a = 0.5 * rho * tworttwoGf # standard matter interaction Hamiltonian clear_matrix(H_mat) # formalism of Hamiltonian: not 1+epsilon_ee^f in [0,0] element but just epsilon... # changed when fitting in Thomas' NSI branch -EL # Obtain effective non-standard matter interaction Hamiltonian # changed when fitting in Thomas' NSI branch -EL for i in range(3): for j in range(3): # matter potential V -> -V* for anti-neutrinos if nubar == -1: H_mat[i, j] = -a * mat_pot[i, j].conjugate() elif nubar == 1: H_mat[i, j] = a * mat_pot[i, j]
def get_H_mat(rho, nsi_eps, nubar, H_mat): ''' Calculate matter Hamiltonian in flavor basis Parameters: ----------- rho : float density nsi_eps : complex 2-d array Non-standard interaction terms nubar : int +1 for neutrinos, -1 for antineutrinos H_mat : complex 2d-array (empty) Notes ----- in the following, `a` is just the standard effective matter potential induced by charged-current weak interactions with electrons ''' # 2*sqrt(2)*Gfermi in (eV^2-cm^3)/(mole-GeV) tworttwoGf = 1.52588e-4 a = 0.5 * rho * tworttwoGf if nubar == -1: a = -a # standard matter interaction Hamiltonian clear_matrix(H_mat) H_mat[0, 0] = a # Obtain effective non-standard matter interaction Hamiltonian nsi_rho_scale = 3. #// assume 3x electron density for "NSI"-quark (e.g., d) density fact = nsi_rho_scale * a for i in range(3): for j in range(3): H_mat[i, j] += fact * nsi_eps[i, j]
def osc_probs_layers_kernel(dm, mix, mat_pot, nubar, energy, density_in_layer, distance_in_layer, osc_probs): """ Calculate oscillation probabilities given layers of length and density Parameters ---------- dm : real 2d array Mass splitting matrix, eV^2 mix : complex 2d array PMNS mixing matrix mat_pot : complex 2d array Generalised matter potential matrix without "a" factor (will be multiplied with "a" factor); set to diag([1, 0, 0]) for only standard oscillations nubar : real int, scalar or Nd array (broadcast dim) 1 for neutrinos, -1 for antineutrinos energy : real float, scalar or Nd array (broadcast dim) Neutrino energy, GeV density_in_layer : real 1d array Density of each layer, moles of electrons / cm^2 distance_in_layer : real 1d array Distance of each layer traversed, km osc_probs : real (N+2)-d array (empty) Returned oscillation probabilities in the form: osc_prob[i,j] = probability of flavor i to oscillate into flavor j with 0 = electron, 1 = muon, 3 = tau Notes ----- !!! Right now, because of CUDA, the maximum number of layers is hard coded and set to 120 (59Layer PREM + Atmosphere). This is used for cached layer computation, where earth layer, which are typically traversed twice (it's symmetric) are not recalculated but rather cached.. """ # 3x3 complex H_vac = cuda.local.array(shape=(3, 3), dtype=ctype) mix_nubar = cuda.local.array(shape=(3, 3), dtype=ctype) mix_nubar_conj_transp = cuda.local.array(shape=(3, 3), dtype=ctype) transition_product = cuda.local.array(shape=(3, 3), dtype=ctype) transition_matrix = cuda.local.array(shape=(3, 3), dtype=ctype) tmp = cuda.local.array(shape=(3, 3), dtype=ctype) clear_matrix(H_vac) clear_matrix(osc_probs) # 3-vector complex raw_input_psi = cuda.local.array(shape=(3), dtype=ctype) output_psi = cuda.local.array(shape=(3), dtype=ctype) use_mass_eigenstates = False cache = True # cache = False # TODO: # * ensure convention below is respected in MC reweighting # (nubar > 0 for nu, < 0 for anti-nu) # * nubar is passed in, so could already pass in the correct form # of mixing matrix, i.e., possibly conjugated if nubar > 0: # in this case the mixing matrix is left untouched copy_matrix(mix, mix_nubar) else: # here we need to complex conjugate all entries # (note that this only changes calculations with non-clear_matrix deltacp) conjugate(mix, mix_nubar) conjugate_transpose(mix_nubar, mix_nubar_conj_transp) get_H_vac(mix_nubar, mix_nubar_conj_transp, dm, H_vac) if cache: # allocate array to store all the transition matrices # doesn't work in cuda...needs fixed shape transition_matrices = cuda.local.array(shape=(120, 3, 3), dtype=ctype) # loop over layers for i in range(distance_in_layer.shape[0]): density = density_in_layer[i] distance = distance_in_layer[i] if distance > 0.0: layer_matrix_index = -1 # chaeck if exists for j in range(i): # if density_in_layer[j] == density and distance_in_layer[j] == distance: if (abs(density_in_layer[j] - density) < 1e-5) and ( abs(distance_in_layer[j] - distance) < 1e-5): layer_matrix_index = j # use from cached if layer_matrix_index >= 0: for j in range(3): for k in range(3): transition_matrices[i, j, k] = transition_matrices[ layer_matrix_index, j, k] # only calculate if necessary else: get_transition_matrix( nubar, energy, density, distance, mix_nubar, mix_nubar_conj_transp, mat_pot, H_vac, dm, transition_matrix, ) # copy for j in range(3): for k in range(3): transition_matrices[i, j, k] = transition_matrix[j, k] else: # identity matrix for j in range(3): for k in range(3): if j == k: transition_matrix[j, k] = 0.0 else: transition_matrix[j, k] = 1.0 # now multiply them all first_layer = True for i in range(distance_in_layer.shape[0]): distance = distance_in_layer[i] if distance > 0.0: for j in range(3): for k in range(3): transition_matrix[j, k] = transition_matrices[i, j, k] if first_layer: copy_matrix(transition_matrix, transition_product) first_layer = False else: matrix_dot_matrix(transition_matrix, transition_product, tmp) copy_matrix(tmp, transition_product) else: # non-cache loop first_layer = True for i in range(distance_in_layer.shape[0]): density = density_in_layer[i] distance = distance_in_layer[i] # only do something if distance > 0. if distance > 0.0: get_transition_matrix( nubar, energy, density, distance, mix_nubar, mix_nubar_conj_transp, mat_pot, H_vac, dm, transition_matrix, ) if first_layer: copy_matrix(transition_matrix, transition_product) first_layer = False else: matrix_dot_matrix(transition_matrix, transition_product, tmp) copy_matrix(tmp, transition_product) # convrt to flavour eigenstate basis matrix_dot_matrix(transition_product, mix_nubar_conj_transp, tmp) matrix_dot_matrix(mix_nubar, tmp, transition_product) # loop on neutrino types, and compute probability for neutrino i: for i in range(3): for j in range(3): raw_input_psi[j] = 0.0 if use_mass_eigenstates: convert_from_mass_eigenstate(i + 1, mix_nubar, raw_input_psi) else: raw_input_psi[i] = 1.0 matrix_dot_vector(transition_product, raw_input_psi, output_psi) osc_probs[i][0] += output_psi[0].real**2 + output_psi[0].imag**2 osc_probs[i][1] += output_psi[1].real**2 + output_psi[1].imag**2 osc_probs[i][2] += output_psi[2].real**2 + output_psi[2].imag**2
def osc_probs_vacuum_kernel(dm, mix, nubar, energy, distance_in_layer, osc_probs): ''' Calculate vacumm mixing probabilities Parameters ---------- dm : 2d-array Mass splitting matrix mix : complex 2d-array PMNS mixing matrix nubar : int +1 for neutrinos, -1 for antineutrinos energy : float Neutrino energy distance_in_layer : 1d-array Baselines (will be summed up) osc_probs : 2d-array (empty) Returned oscillation probabilities in the form: osc_prob[i,j] = probability of flavor i to oscillate into flavor j with 0 = electron, 1 = muon, 3 = tau Notes ----- This is largely unvalidated so far ''' # no need to conjugate mix matrix, as we anyway only need real part # can this be right? clear_matrix(osc_probs) osc_probs_local = cuda.local.array(shape=(3, 3), dtype=ftype) # sum up length from all layers baseline = 0. for i in range(distance_in_layer.shape[0]): baseline += distance_in_layer[i] # make more precise 20081003 rvw l_over_e = 1.26693281 * baseline / energy s21 = math.sin(dm[1, 0] * l_over_e) s32 = math.sin(dm[2, 0] * l_over_e) s31 = math.sin((dm[2, 1] + dm[3, 2]) * l_over_e) # does anybody understand this loop? # ista = abs(*nutype) - 1 for ista in range(3): for iend in range(2): osc_probs_local[ista, iend] = ( (mix[ista, 0].real * mix[ista, 1].real * s21)**2 + (mix[ista, 1].real * mix[ista, 2].real * s32)**2 + (mix[ista, 2].real * mix[ista, 0].real * s31)**2) if iend == ista: osc_probs_local[ista, iend] = 1. - 4. * osc_probs_local[ista, iend] else: osc_probs_local[ista, iend] = -4. * osc_probs_local[ista, iend] osc_probs_local[ista, 2] = 1. - osc_probs_local[ista, 0] - osc_probs_local[ista, 1] # is this necessary? if nubar > 0: copy_matrix(osc_probs_local, osc_probs) else: for i in range(3): for j in range(3): osc_probs[i, j] = osc_probs_local[j, i]