def get_compton_fraction(energy): """ Computes the compton angle from the Klein-Nishina equation. Determines the probability of absorption from this angle. Parameters ---------- energy : float Photon energy Returns ------- compton_angle : float Compton scattering angle compton_fraction : float Fraction of energy lost """ theta_angles, theta_distribution = compton_theta_distribution(energy) z = np.random.random() # get Compton scattering angle compton_angle = np.interp(z, theta_distribution, theta_angles) # Energy calculations fraction = 1 / ( 1.0 + kappa_calculation(energy) * (1.0 - np.cos(compton_angle)) ) return compton_angle, fraction
def photoabsorption_opacity_calculation_kasen( energy, number_density, proton_count ): """Calculates photoabsorption opacity for a given energy Approximate treatment from Kasen et al. (2006) Parameters ---------- energy : float Photon energy number_density : float The number density of the ejecta for each atom proton_count : float Number of protons for each atom in the ejecta Returns ------- float Photoabsorption opacity """ kappa = kappa_calculation(energy) opacity = (FINE_STRUCTURE**4.0) * 8.0 * np.sqrt(2) * (kappa**-3.5) # Note- this should actually be atom_number_density * (atom_proton_number ** 5) return ( SIGMA_T * opacity * np.sum((number_density / proton_count) * proton_count**5) )
def get_compton_angle(energy): """ Computes the compton angle from the Klein-Nishina equation. Computes the lost energy due to this angle Parameters ---------- energy : float Photon energy Returns ------- compton_angle : float Compton scattering angle lost_energy : float Energy lost based on angle new_energy : float Photon energy """ theta_angles, theta_distribution = compton_theta_distribution(energy) z = np.random.random() # get Compton scattering angle compton_angle = np.interp(z, theta_distribution, theta_angles) # Energy calculations new_energy = energy / ( 1.0 + kappa_calculation(energy) * (1.0 - np.cos(compton_angle)) ) lost_energy = energy - new_energy return compton_angle, lost_energy, new_energy
def test_kappa_calculation(energy, expected): """ Parameters ---------- energy : float expected : float """ kappa = util.kappa_calculation(energy) npt.assert_almost_equal(kappa, expected)
def get_compton_fraction_artis(energy): """Gets the Compton scattering/absorption fraction and angle following the scheme in ARTIS Parameters ---------- energy : float Energy of the gamma-ray Returns ------- float Scattering angle float Compton scattering fraction """ energy_norm = kappa_calculation(energy) fraction_max = 1.0 + 2.0 * energy_norm fraction_min = 1.0 normalization = np.random.random() * compton_opacity_partial( energy_norm, fraction_max ) epsilon = 1.0e20 count = 0 while epsilon > 1.0e-4: fraction_try = (fraction_max + fraction_min) / 2.0 sigma_try = compton_opacity_partial(energy_norm, fraction_try) if sigma_try > normalization: fraction_max = fraction_try epsilon = (sigma_try - normalization) / normalization else: fraction_min = fraction_try epsilon = (normalization - sigma_try) / normalization count += 1 if count > 1000: print("Error, failure to get a Compton fraction") break angle = np.arccos(1.0 - ((fraction_try - 1) / energy_norm)) return angle, fraction_try
def test_klein_nishina(energy, theta_C): """ Parameters ---------- energy : float theta_C : float In radians """ actual = util.klein_nishina(energy, theta_C) kappa = util.kappa_calculation(energy) expected = (R_ELECTRON_SQUARED / 2 * (1.0 + kappa * (1.0 - np.cos(theta_C)))**-2.0 * (1.0 + np.cos(theta_C)**2.0 + (kappa**2.0 * (1.0 - np.cos(theta_C))**2.0) / (1.0 + kappa * (1.0 - np.cos(theta_C))))) npt.assert_almost_equal(actual, expected)
def get_average_compton_fraction(energy): def f(x, mu): return 1.0 / (1.0 + x * (1.0 - mu)) def cross_section(x, mu): return ((3.0 * SIGMA_T) / (16.0 * np.pi) * f(x, mu)**2.0 * (f(x, mu) + 1.0 / f(x, mu) - (1.0 - mu**2))) x = kappa_calculation(energy) mus = np.linspace(-1, 1, 100) dmu = mus[1] - mus[0] sum = 0 norm = 0 for mu in mus: sum += cross_section(x, mu) * f(x, mu) * dmu norm += cross_section(x, mu) * dmu integral = 1.0 - sum / norm return 1 - integral
def compton_opacity_calculation(energy, electron_density): """Calculate the Compton scattering opacity for a given energy (Rybicki & Lightman, 1979) $ \\rho / 2 p_m \\times 3/4 \\sigma_T ((1 + \kappa) / \kappa^3 ((2\kappa(1 + \kappa)) / (1 + 2\kappa) - \ln(1 + 2\kappa) + 1/(2\kappa) \ln(1 + 2\kappa) - (1 + 3\kappa) / (1 + 2\kappa)^2) $ Parameters ---------- energy : float The energy of the photon ejecta_density : float The density of the ejecta Returns ------- float The Compton scattering opacity """ kappa = kappa_calculation(energy) a = 1.0 + 2.0 * kappa sigma_KN = ( 3.0 / 4.0 * SIGMA_T * ( (1.0 + kappa) / kappa**3.0 * ((2.0 * kappa * (1.0 + kappa)) / a - np.log(a)) + 1.0 / (2.0 * kappa) * np.log(a) - (1.0 + 3 * kappa) / a**2.0 ) ) return electron_density * sigma_KN
def get_compton_fraction_urilight(energy): """Gets the Compton scattering/absorption fraction and angle following the scheme in Urilight Parameters ---------- energy : float Energy of the gamma-ray Returns ------- float Scattering angle float Compton scattering fraction """ E0 = kappa_calculation(energy) x0 = 1.0 / (1.0 + 2.0 * E0) accept = False while not accept: z = np.random.random(3) alpha1 = np.log(1.0 / x0) alpha2 = (1.0 - x0**2.0) / 2.0 if z[1] < alpha1 / (alpha1 + alpha2): x = x0 ** z[2] else: x = np.sqrt(x0**2.0 + (1.0 - x0**2.0) * z[2]) f = (1.0 - x) / x / E0 sin2t = f * (2.0 - f) ge = 1.0 - x / (1 + x**2.0) * sin2t if ge > z[3]: accept = True cost = 1.0 - f return np.arccos(cost), x
def gamma_packet_loop( packets, grey_opacity, photoabsorption_opacity_type, pair_creation_opacity_type, electron_number_density_time, mass_density_time, inv_volume_time, iron_group_fraction_per_shell, inner_velocities, outer_velocities, times, dt_array, effective_time_array, energy_bins, energy_df_rows, energy_plot_df_rows, energy_out, ): """Propagates packets through the simulation Parameters ---------- packets : list List of GXPacket objects grey_opacity : float Grey opacity value in cm^2/g electron_number_density_time : array float64 Electron number densities with time mass_density_time : array float64 Mass densities with time inv_volume_time : array float64 Inverse volumes with time iron_group_fraction_per_shell : array float64 Iron group fraction per shell inner_velocities : array float64 Inner velocities of the shells outer_velocities : array float64 Inner velocities of the shells times : array float64 Simulation time steps dt_array : array float64 Simulation delta-time steps effective_time_array : array float64 Simulation middle time steps energy_bins : array float64 Bins for escaping gamma-rays energy_df_rows : array float64 Energy output energy_plot_df_rows : array float64 Energy output for plotting energy_out : array float64 Escaped energy array Returns ------- array float64 Energy output array float64 Energy output for plotting array float64 Escaped energy array Raises ------ ValueError Packet time index less than zero """ escaped_packets = 0 scattered_packets = 0 packet_count = len(packets) print("Entering gamma ray loop for " + str(packet_count) + " packets") deposition_estimator = np.zeros_like(energy_df_rows) for i in range(packet_count): packet = packets[i] time_index = get_index(packet.time_current, times) if time_index < 0: print(packet.time_current, time_index) raise ValueError("Packet time index less than 0!") scattered = False initial_energy = packet.energy_cmf while packet.status == GXPacketStatus.IN_PROCESS: # Get delta-time value for this step dt = dt_array[time_index] # Calculate packet comoving energy for opacities comoving_energy = H_CGS_KEV * packet.nu_cmf if grey_opacity < 0: doppler_factor = doppler_gamma( packet.direction, packet.location, effective_time_array[time_index], ) kappa = kappa_calculation(comoving_energy) # artis threshold for Thomson scattering if kappa < 1e-2: compton_opacity = ( SIGMA_T * electron_number_density_time[packet.shell, time_index]) else: compton_opacity = compton_opacity_calculation( comoving_energy, electron_number_density_time[packet.shell, time_index], ) if photoabsorption_opacity_type == "kasen": # currently not functional, requires proton count and # electron count per isotope photoabsorption_opacity = 0 # photoabsorption_opacity_calculation_kasen() else: photoabsorption_opacity = ( photoabsorption_opacity_calculation( comoving_energy, mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], )) if pair_creation_opacity_type == "artis": pair_creation_opacity = pair_creation_opacity_artis( comoving_energy, mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], ) else: pair_creation_opacity = pair_creation_opacity_calculation( comoving_energy, mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], ) else: compton_opacity = 0.0 pair_creation_opacity = 0.0 photoabsorption_opacity = ( grey_opacity * mass_density_time[packet.shell, time_index]) # convert opacities to rest frame total_opacity = (compton_opacity + photoabsorption_opacity + pair_creation_opacity) * doppler_factor packet.tau = -np.log(np.random.random()) ( distance_interaction, distance_boundary, distance_time, shell_change, ) = distance_trace( packet, inner_velocities, outer_velocities, total_opacity, effective_time_array[time_index], times[time_index + 1], ) distance = min(distance_interaction, distance_boundary, distance_time) packet.time_current += distance / C_CGS packet = move_packet(packet, distance) deposition_estimator[packet.shell, time_index] += ( (initial_energy * 1000) * distance * (packet.energy_cmf / initial_energy) * deposition_estimator_kasen( comoving_energy, mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], )) if distance == distance_time: time_index += 1 if time_index > len(effective_time_array) - 1: # Packet ran out of time packet.status = GXPacketStatus.END else: packet.shell = get_index( packet.get_location_r(), inner_velocities * effective_time_array[time_index], ) elif distance == distance_interaction: packet.status = scatter_type( compton_opacity, photoabsorption_opacity, total_opacity, ) packet, ejecta_energy_gained = process_packet_path(packet) # Save packets to dataframe rows # convert KeV to eV / s / cm^3 energy_df_rows[packet.shell, time_index] += (ejecta_energy_gained * 1000) energy_plot_df_rows[i] = np.array([ i, ejecta_energy_gained * 1000 # * inv_volume_time[packet.shell, time_index] / dt, packet.get_location_r(), packet.time_current, packet.shell, compton_opacity, photoabsorption_opacity, pair_creation_opacity, ]) if packet.status == GXPacketStatus.PHOTOABSORPTION: # Packet destroyed, go to the next packet break else: packet.status = GXPacketStatus.IN_PROCESS scattered = True else: packet.shell += shell_change if packet.shell > len(mass_density_time[:, 0]) - 1: rest_energy = packet.nu_rf * H_CGS_KEV bin_index = get_index(rest_energy, energy_bins) bin_width = (energy_bins[bin_index + 1] - energy_bins[bin_index]) energy_out[bin_index, time_index] += rest_energy / (bin_width * dt) packet.status = GXPacketStatus.END escaped_packets += 1 if scattered: scattered_packets += 1 elif packet.shell < 0: packet.energy_rf = 0.0 packet.energy_cmf = 0.0 packet.status = GXPacketStatus.END print("Escaped packets:", escaped_packets) print("Scattered packets:", scattered_packets) return energy_df_rows, energy_plot_df_rows, energy_out, deposition_estimator