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 sum_row_kernel(mix, bla, inp, out): C = cuda.local.array(shape=(3, 3), dtype=ftype) D = cuda.local.array(shape=(3), dtype=ctype) E = cuda.local.array(shape=(3), dtype=ctype) matrix_dot_matrix(mix, mix, C) D[0] = 0.0 + 2.0j D[1] = 1.0 + 2.0j D[2] = 1.0 + 2.0j matrix_dot_vector(C, D, E) bla *= 0.1 out[0] += E[1].real * bla.real + inp[0]
def get_transition_matrix( nubar, energy, rho, baseline, mix_nubar, mix_nubar_conj_transp, mat_pot, H_vac, dm, transition_matrix, ): """ Calculate neutrino flavour transition amplitude matrix Parameters ---------- nubar : int +1 for neutrinos, -1 for antineutrinos energy : real float Neutrino energy, GeV 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) baseline : real float Baseline, km mix_nubar : complex 2d array Mixing matrix, already conjugated if antineutrino mix_nubar_conj_transp : complex conjugate 2d array Conjugate transpose of mix_nubar 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 H_vac : complex 2d array Hamiltonian in vacuum, without the 1/2E term dm : real 2d array Mass splitting matrix, eV^2 transition_matrix : complex 2d array (empty) Transition matrix in mass eigenstate basis Notes ----- For neutrino (nubar > 0) or antineutrino (nubar < 0) with energy energy traversing layer of matter of uniform density rho with thickness baseline """ H_mat = cuda.local.array(shape=(3, 3), dtype=ctype) dm_mat_vac = cuda.local.array(shape=(3, 3), dtype=ctype) dm_mat_mat = cuda.local.array(shape=(3, 3), dtype=ctype) H_full = cuda.local.array(shape=(3, 3), dtype=ctype) tmp = cuda.local.array(shape=(3, 3), dtype=ctype) H_mat_mass_eigenstate_basis = cuda.local.array(shape=(3, 3), dtype=ctype) # Compute the matter potential including possible generalized interactions # in the flavor basis get_H_mat(rho, mat_pot, nubar, H_mat) # Get the full Hamiltonian by adding together matter and vacuum parts one_over_two_e = 0.5 / energy for i in range(3): for j in range(3): H_full[i, j] = H_vac[i, j] * one_over_two_e + H_mat[i, j] # Calculate modified mass eigenvalues in matter from the full Hamiltonian and # the vacuum mass splittings get_dms(energy, H_full, dm, dm_mat_mat, dm_mat_vac) # Now we transform the matter (TODO: matter? full?) Hamiltonian back into the # mass eigenstate basis so we don't need to compute products of the effective # mixing matrix elements explicitly matrix_dot_matrix(H_mat, mix_nubar, tmp) matrix_dot_matrix(mix_nubar_conj_transp, tmp, H_mat_mass_eigenstate_basis) # We can now proceed to calculating the transition amplitude from the Hamiltonian # in the mass basis and the effective mass splittings get_transition_matrix_massbasis( baseline, energy, dm_mat_vac, dm_mat_mat, H_mat_mass_eigenstate_basis, transition_matrix, )
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 get_transition_matrix(nubar, energy, rho, baseline, mix_nubar, mix_nubar_conj_transp, nsi_eps, H_vac, dm, transition_matrix): ''' Calculate neutrino flavour transition amplitude matrix Parameters ---------- nubar : int energy : float baseline : float mix_nubar : 2d-array Mixing matrix, already conjugated if antineutrino mix_nubar_conj_transp : comjugate 2d-array conjugate transpose of mixing matrix nsi_eps : 2d-array H_vac : 2d-array dm : 2d-array transition_matrix : 2d-array (empty) in mass eigenstate basis Notes ----- for neutrino (nubar > 0) or antineutrino (nubar < 0) with energy energy traversing layer of matter of uniform density rho with thickness baseline ''' H_mat = cuda.local.array(shape=(3, 3), dtype=ctype) dm_mat_vac = cuda.local.array(shape=(3, 3), dtype=ctype) dm_mat_mat = cuda.local.array(shape=(3, 3), dtype=ctype) H_full = cuda.local.array(shape=(3, 3), dtype=ctype) tmp = cuda.local.array(shape=(3, 3), dtype=ctype) H_mat_mass_eigenstate_basis = cuda.local.array(shape=(3, 3), dtype=ctype) # Compute the matter potential including possible non-standard interactions # in the flavor basis get_H_mat(rho, nsi_eps, nubar, H_mat) # Get the full Hamiltonian by adding together matter and vacuum parts one_over_two_e = 0.5 / energy for i in range(3): for j in range(3): H_full[i, j] = H_vac[i, j] * one_over_two_e + H_mat[i, j] # Calculate modified mass eigenvalues in matter from the full Hamiltonian and # the vacuum mass splittings get_dms(energy, H_full, dm, dm_mat_mat, dm_mat_vac) # Now we transform the matter (TODO: matter? full?) Hamiltonian back into the # mass eigenstate basis so we don't need to compute products of the effective # mixing matrix elements explicitly matrix_dot_matrix(H_mat, mix_nubar, tmp) matrix_dot_matrix(mix_nubar_conj_transp, tmp, H_mat_mass_eigenstate_basis) # We can now proceed to calculating the transition amplitude from the Hamiltonian # in the mass basis and the effective mass splittings get_transition_matrix_massbasis(baseline, energy, dm_mat_vac, dm_mat_mat, H_mat_mass_eigenstate_basis, transition_matrix)