def get_kappa_2s(photspec): """ Compute kappa_2s for use in kappa_DM function Parameters ---------- photspec : Spectrum object spectrum of photons. spec.toteng() should return Energy per baryon. Returns ------- kappa_2s : float The added photoionization rate from the 1s to the 2s state due to DM photons. """ # Convenient Variables eng = photspec.eng rs = photspec.rs Lambda = phys.width_2s1s_H Tcmb = phys.TCMB(rs) lya_eng = phys.lya_eng # Photon phase space density (E >> kB*T approximation) def Boltz(E): return np.exp(-E / Tcmb) bounds = spectools.get_bin_bound(eng) mid = spectools.get_indx(bounds, lya_eng / 2) # Phase Space Density of DM f_nu = photspec.dNdE * phys.c**3 / (8 * np.pi * (eng / phys.hbar)**2) # Complementary (E - h\nu) phase space density of DM f_nu_p = np.zeros(mid) # Index of point complementary to eng[k] comp_indx = spectools.get_indx(bounds, lya_eng - eng[0]) # Find the bin in which lya_eng - eng[k] resides. Store f_nu of that bin in f_nu_p. for k in np.arange(mid): while (lya_eng - eng[k]) < bounds[comp_indx]: comp_indx -= 1 f_nu_p[k] = f_nu[comp_indx] # Setting up the numerical integration # Bin sizes diffs = np.append(bounds[1:mid], lya_eng / 2) - np.insert( bounds[1:mid], 0, 0) diffs /= (2 * np.pi * phys.hbar) dLam_dnu = phys.get_dLam2s_dnu() rates = dLam_dnu(eng[:mid] / (2 * np.pi * phys.hbar)) boltz = Boltz(eng[:mid]) boltz_p = Boltz(lya_eng - eng[:mid]) # The Numerical Integral kappa_2s = np.sum(diffs * rates * (f_nu[:mid] + boltz) * (f_nu_p + boltz_p)) / phys.width_2s1s_H - Boltz(lya_eng) return kappa_2s
def ionize_s_cs_H_2(E_in, E_sec): ''' Calculates the integrated, singly-differential ionization cross section (xsec) for electrons impacting H at a particular kinetic energy of the incident over a log-space vector of secondary energies. Parameters ---------- E_in : float Primary electron's initial kinetic energy (eV). E_sec : ndarray ([Spectrum].eng) The energy of one secondary electron for each initial electron (eV). Returns ---------- sigma : ndarray The cross section for ionization integrated over a log-bin centered at each secondary energy (E_sec[n]); (given in cm^2). See Also -------- ionize_cs : Gives total ionization xsec ionize_s_cs: Gives individual values for multi-atom singly-diff xsecs ''' #initialize return variable sigma = numpy.zeros(len(E_sec)) #get bin boundaries for integration edges = spectools.get_bin_bound(E_sec) def integrand(W): #W=E_sec[n] T = E_in B = 13.6057 #eV: binding energy U = 13.6057 #eV: t = T / B w = W / B y = 1 / (w + 1) df_dw = -0.022473 * y**2 + 1.1775 * y**3 - 0.46264 * y**4 + 0.089064 * y**5 N = 1 # number of bound electrons in subshell N_i = 0.4343 #integral of df/dw from 0 to infinity u = U / B S = 4 * math.pi * p.value('Bohr radius')**2 * N * (13.6057 / B)**2 #m^2 return S / (B * (t + (u + 1))) * ( (N_i / N - 2) / (t + 1) * (1 / (w + 1) + 1 / (t - w)) * (2 - N_i / N) * (1 / (w + 1)**2 + 1 / (t - w)**2) + numpy.log(t) / (N * (w + 1)) * df_dw) #cm^2/eV #generates vector for summation integrand_vec = np.zeros(len(E_sec)) #change begins B = 13.6057 f_max = (E_in - B) / 2 for i, E_s in enumerate(E_sec): if E_s > E_in - B: integrand_vec[i] = 0 elif E_s > f_max: f_delta = E_s - f_max integrand_vec[i] = integrand(E_s - 2 * f_delta) else: integrand_vec[i] = integrand(E_s) #change ends #performs summation for n in range(len(E_sec) - 1): sigma[n] = integrate.trapz(integrand_vec[n:n + 2], E_sec[n:n + 2]) #integrates continuously #for n in range(len(E_sec)): #sigma[n] = integrate.quad(integrand, edges[n], edges[n+1])[0] sigma = sigma.clip(min=0) return sigma
def compute_dep_inj_ionization_ratio(photon_spectrum, n, tot_inj, method='old'): """ Given a spectrum of deposited photons, resolve its energy into continuum photons, HI excitation, and HI, HeI, HeII ionization in that order. The spectrum must provide the energy density of photons per unit time within each bin, not just the total energy within each bin. Q: can photons heat the IGM? Should this method keep track of the fact that x_e, xHII, etc. are changing? Parameters ---------- photon_spectrum : Spectrum object spectrum of photons n : list of floats density of (HI, HeI, HeII). tot_inj : float total energy injected by DM method : {'old','ion','new'} 'old': All photons >= 13.6eV ionize hydrogen, within [10.2, 13.6)eV excite hydrogen, < 10.2eV are labelled continuum. 'ion': Same as 'old', but now photons >= 13.6 can ionize HeI and HeII also. 'new': Same as 'ion', but now [10.2, 13.6)eV photons treated more carefully Returns ------- tuple of floats Ratio of deposited energy to a given channel over energy deposited by DM. The order of the channels is HI excitation and HI, HeI, HeII ionization """ continuum, excite_HI, f_HI, f_HeI, f_HeII = 0, 0, 0, 0, 0 continuum = photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([ photon_spectrum.eng[0], phys.lya_eng ]))[0] / tot_inj ion_index = np.searchsorted(photon_spectrum.eng, phys.rydberg) if (method != 'new'): excite_HI = photon_spectrum.toteng( bound_type='eng', bound_arr=np.array([phys.lya_eng, phys.rydberg ]))[0] / tot_inj if (method == 'old'): f_HI = photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([ phys.rydberg, photon_spectrum.eng[-1] ]))[0] / tot_inj elif (method == 'ion'): # probability of being absorbed within time step dt in channel a = \sigma(E)_a n_a c*dt # First convert from probability of being absorbed in channel 'a' to conditional probability given that these are deposited photons # TODO: could be improved to include the missing [13.6,ion_bin] ionHI, ionHeI, ionHeII = [ phys.photo_ion_xsec(photon_spectrum.eng[ion_index:], channel) * n[i] for i, channel in enumerate(['H0', 'He0', 'He1']) ] totList = ionHI + ionHeI + ionHeII ion_bin = spectools.get_bin_bound(photon_spectrum.eng)[ion_index] print(ion_bin) print((sum(photon_spectrum.eng[ion_index:] * photon_spectrum.N[ion_index:]) + photon_spectrum.toteng( bound_type='eng', bound_arr=np.array( [photon_spectrum.eng[0], phys.rydberg]))[0] + photon_spectrum.toteng( bound_type='eng', bound_arr=np.array([phys.rydberg, ion_bin]))[0]) / tot_inj) temp = np.array(photon_spectrum.eng[ion_index:]) np.insert(temp, 0, phys.rydberg) print((photon_spectrum.toteng( bound_type='eng', bound_arr=np.array([photon_spectrum.eng[0], phys.rydberg]))[0] + sum(photon_spectrum.toteng(bound_type='eng', bound_arr=temp))) / tot_inj) print( sum( photon_spectrum.toteng(bound_type='eng', bound_arr=photon_spectrum.eng)) / tot_inj) f_HI, f_HeI, f_HeII = [ sum(photon_spectrum.eng[ion_index:] * photon_spectrum.N[ion_index:] * llist / totList) / tot_inj for llist in [ionHI, ionHeI, ionHeII] ] #f_HI, f_HeI, f_HeII = [sum(photon_spectrum.toteng(bound_type='eng',bound_arr=np.arange(ion_index-1,len(photon_spectrum.eng)))*llist/totList)/tot_inj for llist in [ionHI, ionHeI, ionHeII]] #There's an extra piece of energy between 13.6 amd the energy at ion_index #print(photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([phys.rydberg,photon_spectrum.eng[ion_index]]))[0]/tot_inj) #f_HI = f_HI + photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([phys.rydberg,photon_spectrum.eng[ion_index]]))[0]/tot_inj return continuum, excite_HI, f_HI, f_HeI, f_HeII
def toteng(self, bound_type=None, bound_arr=None): """Returns the total energy of particles in part of the spectra. The part of the `Spectrum` objects to find the total energy of particles can be specified in two ways, and is specified by `bound_type`. Multiple totals can be obtained through `bound_arr`. Parameters ---------- bound_type : {'bin', 'eng', None} The type of bounds to use. Bound values do not have to be within the [0:eng.size] for `'bin'` or within the abscissa for `'eng'`. `None` should only be used when computing the total particle number in the spectrum. Specifying ``bound_type=='bin'`` without bound_arr gives the total energy in each bin. bound_arr : ndarray of length N, optional An array of boundaries (bin or energy), between which the total number of particles will be computed. If unspecified, the total number of particles in the whole spectrum is computed. For 'bin', bounds are specified as the bin *boundary*, with 0 being the left most boundary, 1 the right-hand of the first bin and so on. This is equivalent to integrating over a histogram. For 'eng', bounds are specified by energy values. These boundaries need not be integer values for 'bin': specifying np.array([0.5, 1.5]) for example will include half of the first bin and half of the second. Returns ------- ndarray of shape (self.rs.size, N-1) or length N-1 Total number of particles in the spectra or between the specified boundaries. Examples -------- >>> from darkhistory.spec.spectrum import Spectrum >>> eng = np.array([1, 10, 100, 1000]) >>> spec_arr = [Spectrum(eng, np.arange(4) + 4*i, rs=100, spec_type='N') for i in np.arange(4)] >>> test_spectra = Spectra(spec_arr) >>> test_spectra.toteng() array([ 3210., 7654., 12098., 16542.]) >>> test_spectra.toteng('bin', np.array([1, 3])) array([[ 210., 650., 1090., 1530.]]) >>> test_spectra.toteng('eng', np.array([10, 1e4])) array([[ 3205., 7625., 12045., 16465.]]) See Also --------- :meth:`Spectra.totN` """ log_bin_width = get_log_bin_width(self.eng) # Using the broadcasting rules here. if self.spec_type == 'dNdE': dNdlogE = self.grid_vals * self.eng elif self.spec_type == 'N': dNdlogE = self.grid_vals / log_bin_width if bound_type is not None: if bound_arr is None: return dNdlogE * self.eng * log_bin_width if bound_type == 'bin': if not all(np.diff(bound_arr) >= 0): raise TypeError('bound_arr must have increasing entries.') # Size is number of totals requested x number of Spectrums. eng_in_bin = np.zeros((bound_arr.size - 1, self.in_eng.size)) if bound_arr[0] > self.eng.size or bound_arr[-1] < 0: return eng_in_bin for i, (low, upp) in enumerate(zip(bound_arr[:-1], bound_arr[1:])): # Set the lower and upper bounds, including case where # low and upp are outside of the bins. if low > self.eng.size or upp < 0: continue low_ceil = int(np.ceil(low)) low_floor = int(np.floor(low)) upp_ceil = int(np.ceil(upp)) upp_floor = int(np.floor(upp)) # Sum the bins that are completely between the bounds. eng_full_bins = np.dot( dNdlogE[:, low_ceil:upp_floor] * self.eng[low_ceil:upp_floor], log_bin_width[low_ceil:upp_floor]) eng_part_bins = np.zeros_like(self.in_eng) if low_floor == upp_floor or low_ceil == upp_ceil: # Bin indices are within the same bin. The second # requirement covers the case where upp_ceil is # eng.size. eng_part_bins += (dNdlogE[:, low_floor] * (upp - low) * self.eng[low_floor] * log_bin_width[low_floor]) else: # Add up part of the bin for the low partial bin # and the high partial bin. eng_part_bins += (dNdlogE[:, low_floor] * (low_ceil - low) * self.eng[low_floor] * log_bin_width[low_floor]) if upp_floor < self.eng.size: # If upp_floor is eng.size, then there is no # partial bin for the upper index. eng_part_bins += (dNdlogE[:, upp_floor] * (upp - upp_floor) * self.eng[upp_floor] * log_bin_width[upp_floor]) eng_in_bin[i] = eng_full_bins + eng_part_bins return eng_in_bin if bound_type == 'eng': bin_boundary = get_bin_bound(self.eng) eng_bin_ind = np.interp(np.log(bound_arr), np.log(bin_boundary), np.arange(bin_boundary.size), left=0, right=self.eng.size + 1) return self.toteng('bin', eng_bin_ind) else: return (np.dot(dNdlogE, self.eng * log_bin_width) + self.eng_underflow)
def toteng(self, bound_type=None, bound_arr=None): """Returns the total energy of particles in part of the spectrum. The part of the spectrum can be specified in two ways, and is specified by bound_type. Multiple totals can be obtained through bound_arr. Parameters ---------- bound_type : {'bin', 'eng', None} The type of bounds to use. Bound values do not have to be within the [0:length] for 'bin' or within the abscissa for 'eng'. None should only be used to obtain the total energy. Specifying ``bound_type='bin'`` without bound_arr gives the total energy in each bin. bound_arr : ndarray of length N, optional An array of boundaries (bin or energy), between which the total number of particles will be computed. If unspecified, the total number of particles in the whole spectrum is computed. For 'bin', bounds are specified as the bin *boundary*, with 0 being the left most boundary, 1 the right-hand of the first bin and so on. This is equivalent to integrating over a histogram. For 'eng', bounds are specified by energy values. These boundaries need not be integer values for 'bin': specifying np.array([0.5, 1.5]) for example will include half of the first bin and half of the second. Returns ------- ndarray of length N-1, or float Total energy in the spectrum or between the specified boundaries. Examples --------- >>> eng = np.array([1, 10, 100, 1000]) >>> N = np.array([1, 2, 3, 4]) >>> spec = Spectrum(eng, N, spec_type='N') >>> spec.toteng() 4321.0 >>> spec.toteng('bin', np.array([1, 3])) array([320.]) >>> spec.toteng('eng', np.array([10, 1e4])) array([4310.]) See Also --------- :meth:`.Spectrum.totN` """ eng = self.eng length = self.length log_bin_width = get_log_bin_width(self.eng) if self._spec_type == 'dNdE': dNdlogE = self.eng * self.dNdE elif self._spec_type == 'N': dNdlogE = self.N / log_bin_width if bound_type is not None: if bound_arr is None: return dNdlogE * eng * log_bin_width if bound_type == 'bin': if not all(np.diff(bound_arr) >= 0): raise TypeError("bound_arr must have increasing entries.") eng_in_bin = np.zeros(bound_arr.size - 1) if bound_arr[0] > length or bound_arr[-1] < 0: return eng_in_bin for low, upp, i in zip(bound_arr[:-1], bound_arr[1:], np.arange(eng_in_bin.size)): if low > length or upp < 0: eng_in_bin[i] = 0 continue low_ceil = int(np.ceil(low)) low_floor = int(np.floor(low)) upp_ceil = int(np.ceil(upp)) upp_floor = int(np.floor(upp)) # Sum the bins that are completely between the bounds. eng_full_bins = np.dot( eng[low_ceil:upp_floor] * dNdlogE[low_ceil:upp_floor], log_bin_width[low_ceil:upp_floor]) eng_part_bins = 0 if low_floor == upp_floor or low_ceil == upp_ceil: # Bin indices are within the same bin. The second requirement covers the case where upp_ceil is length. eng_part_bins += (eng[low_floor] * dNdlogE[low_floor] * (upp - low) * log_bin_width[low_floor]) else: # Add up part of the bin for the low partial bin and the high partial bin. eng_part_bins += (eng[low_floor] * dNdlogE[low_floor] * (low_ceil - low) * log_bin_width[low_floor]) if upp_floor < length: # If upp_floor is length, then there is no partial bin for the upper index. eng_part_bins += (eng[upp_floor] * dNdlogE[upp_floor] * (upp - upp_floor) * log_bin_width[upp_floor]) eng_in_bin[i] = eng_full_bins + eng_part_bins return eng_in_bin if bound_type == 'eng': bin_boundary = get_bin_bound(self.eng) eng_bin_ind = np.interp(np.log(bound_arr), np.log(bin_boundary), np.arange(bin_boundary.size), left=0, right=length + 1) return self.toteng('bin', eng_bin_ind) else: return (np.dot(dNdlogE, eng * log_bin_width) + self.underflow['eng'])