def dyHeII_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) if chi - xHeII(yHeII) < 1e-6 and rs < 100: # At this point, leave at 1 - 1e-6 return 0 xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) ne = xe * nH xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) return 2 / chi * np.cosh(yHeII)**2 * phys.dtdz(rs) * ( # Photoionization of HeI into HeII. xHeI * photoion_rate_HeI(rs) # Collisional ionization of HeI to HeII. + xHeI * ne * reion.coll_ion_rate('HeI', T_m) # Recombination of HeIII to HeII. + xHeIII(yHeIII) * ne * reion.alphaA_recomb('HeIII', T_m) # Photoionization of HeII to HeIII. - xHeII(yHeII) * photoion_rate_HeII(rs) # Collisional ionization of HeII to HeIII. - xHeII(yHeII) * ne * reion.coll_ion_rate('HeII', T_m) # Recombination of HeII into HeI. - xHeII(yHeII) * ne * reion.alphaA_recomb('HeII', T_m) # DM contribution + _f_He_ion(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate / (phys.He_ion_eng * nH))
def dyHII_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) if 1 - xHII(yHII) < 1e-6 and rs < 100: # At this point, leave at 1 - 1e-6 return 0 xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) ne = xe * nH xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) return 2 * np.cosh(yHII)**2 * phys.dtdz(rs) * ( # DM injection. Note that C = 1 at late times. +_f_H_ion(rs, xHI, xHeI, xHeII(yHeII)) * (inj_rate / (phys.rydberg * nH)) + (1 - phys.peebles_C(xHII(yHII), rs)) * (_f_H_exc(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate / (phys.lya_eng * nH)) # Reionization rates. + ( # Photoionization. xHI * photoion_rate_HI(rs) # Collisional ionization. + xHI * ne * reion.coll_ion_rate('HI', T_m) # Recombination. - xHII(yHII) * ne * reion.alphaA_recomb('HII', T_m)))
def dlogTb_dz(yHII, log_Tb, rs): Tb = np.exp(log_Tb) xHII = get_x(yHII) xHI = 1 - xHII adia = 2 / rs denom = 3 / 2 * Tb * nH * (1 + chi + xe) comp = phys.dtdz(rs) * compton_cooling_rate(xHII, Tb, rs) / denom return adia + comp
def dyHeII_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) if not helium_TLA: return 0 if chi - xHeII(yHeII) < 1e-6 and rs < 100: # At this point, leave at 1 - 1e-6 return 0 # Stop the solver from reaching these extremes. if yHeII > 14 or yHeII < -14: return 0 # # Use the Saha values at high ionization. # if xHeII(yHeII) > 0.995*chi: # # print(phys.d_xe_Saha_dz(rs, 'HeI')) # return ( # 2/chi * np.cosh(yHeII)**2 * phys.d_xe_Saha_dz(rs, 'HeI') # ) xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) ne = xe * nH xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) term_recomb_singlet = (xHeII(yHeII) * xe * nH * phys.alpha_recomb(T_m, 'HeI_21s')) term_ion_singlet = ( phys.beta_ion(phys.TCMB(rs), 'HeI_21s') * (chi - xHeII(yHeII)) * np.exp(-phys.He_exc_eng['21s'] / phys.TCMB(rs))) term_recomb_triplet = (xHeII(yHeII) * xe * nH * phys.alpha_recomb(T_m, 'HeI_23s')) term_ion_triplet = ( 3 * phys.beta_ion(phys.TCMB(rs), 'HeI_23s') * (chi - xHeII(yHeII)) * np.exp(-phys.He_exc_eng['23s'] / phys.TCMB(rs))) return 2 / chi * np.cosh(yHeII)**2 * phys.dtdz(rs) * ( -phys.C_He(xHII(yHII), xHeII(yHeII), rs, 'singlet') * (term_recomb_singlet - term_ion_singlet) - phys.C_He(xHII(yHII), xHeII(yHeII), rs, 'triplet') * (term_recomb_triplet - term_ion_triplet) + _f_He_ion(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate / (phys.He_ion_eng * nH))
def dlogT_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) # This rate is temperature loss per redshift. adiabatic_cooling_rate = 2 * T_m / rs # The reionization rates and the Compton rate # are expressed in *energy loss* *per second*. photoheat_total_rate = nH * ( xHI * photoheat_rate_HI(rs) + xHeI * photoheat_rate_HeI(rs) + xHeII(yHeII) * photoheat_rate_HeII(rs)) compton_rate = phys.dtdz(rs) * (compton_cooling_rate( xHII(yHII), xHeII(yHeII), xHeIII(yHeIII), T_m, rs)) / (3 / 2 * nH * (1 + chi + xe)) dm_heating_rate = phys.dtdz(rs) * (_f_heating( rs, xHI, xHeI, xHeII(yHeII)) * inj_rate) / (3 / 2 * nH * (1 + chi + xe)) reion_rate = phys.dtdz(rs) * ( +photoheat_total_rate + reion.recomb_cooling_rate( xHII(yHII), xHeII(yHeII), xHeIII(yHeIII), T_m, rs) + reion.coll_ion_cooling_rate(xHII(yHII), xHeII(yHeII), xHeIII(yHeIII), T_m, rs) + reion.coll_exc_cooling_rate(xHII(yHII), xHeII(yHeII), xHeIII(yHeIII), T_m, rs) + reion.brem_cooling_rate(xHII(yHII), xHeII(yHeII), xHeIII( yHeIII), T_m, rs)) / (3 / 2 * nH * (1 + chi + xe)) return 1 / T_m * (adiabatic_cooling_rate + compton_rate + dm_heating_rate + reion_rate)
def dlogTDM_dz(y_be, log_TDM, rs): #rs = np.exp(logrs) T_DM = np.exp(log_TDM) x_be = get_x(y_be) eps = 1 - T_DM / T_D # Cooling rate due to adiabatic expansion adia = 2 / rs #Compton comp = phys.dtdz(rs) * norm_compton_cooling_rate( x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * (T_D / T_DM - 1.) #Rayleigh Rayl = phys.dtdz(rs) * Gam_R(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * (T_D / T_DM - 1) #brem heating + ion heating - recomb cooling ff_pi_pr = phys.dtdz(rs) * ( Gam_ff(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * eps + Gam_pi(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) - Gam_pr(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) - 0) deriv = Rayl + comp + adia + ff_pi_pr #Baryon-DM energy exchange V_pec = 0 fDM = 2 * x_be xsec = None #DM baryon scattering cross-section #baryon = phys.dtdz(rs) * DM_IGM_cooling_rate( # m_be, m_bp, phys.TCMB(rs), T_DM, V_pec, x_be, rs, # fDM, particle_type='DM', eps=eps #)/(3/2 * n_D * (1 + x_be)) return deriv
def dybe_dz(y_be, log_T, rs): #rs = np.exp(logrs) T = np.exp(log_T) x_be = get_x(y_be) xD = 1 - x_be if x_be > 0.999 and rs > 2000: # Use the Saha value. dxdz = d_x_be_Saha_dz(rs, alphaD, m_be, m_bp, xi) return 2 * np.cosh(y_be)**2 * dxdz peeb_C = dark_peebles_C(x_be, rs, alphaD, m_be, m_bp, xi) alpha = dark_alpha_recomb(T, alphaD, m_be, m_bp, xi) beta = dark_beta_ion(T_D, alphaD, m_be, m_bp, xi) return 2 * np.cosh(y_be)**2 * phys.dtdz(rs) * ( -peeb_C * (alpha * x_be**2 * n_D - 4 * beta * xD * np.exp(-Lya_D / T_D)))
def dlogT_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) # This rate is temperature loss per redshift. adiabatic_cooling_rate = 2 * T_m / rs return 1 / T_m * adiabatic_cooling_rate + 1 / T_m * ( phys.dtdz(rs) * (compton_cooling_rate(xHII(yHII), xHeII(yHeII), xHeIII(yHeIII), T_m, rs) + _f_heating(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate)) / ( 3 / 2 * nH * (1 + chi + xe))
def dyHeIII_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) if chi - xHeIII(yHeIII) < 1e-6 and rs < 100: # At this point, leave at 1 - 1e-6 return 0 xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) ne = xe * nH return 2 / chi * np.cosh(yHeIII)**2 * phys.dtdz(rs) * ( # Photoionization of HeII into HeIII. xHeII(yHeII) * photoion_rate_HeII(rs) # Collisional ionization of HeII into HeIII. + xHeII(yHeII) * ne * reion.coll_ion_rate('HeII', T_m) # Recombination of HeIII into HeII. - xHeIII(yHeIII) * ne * reion.alphaA_recomb('HeIII', T_m))
def dlogT_dz(log_T_m, rs): T_m = np.exp(log_T_m) xe = xe_reion_func(rs) xHII = xe * (1. / (1. + chi)) xHeII = xe * (chi / (1. + chi)) xHI = 1. - xHII xHeI = chi - xHeII # This is the temperature loss per redshift. adiabatic_cooling_rate = 2 * T_m / rs return 1 / T_m * ( adiabatic_cooling_rate + (phys.dtdz(rs) * (compton_cooling_rate(xHII, xHeII, 0, T_m, rs) + _f_heating(rs, xHI, xHeI, 0) * _injection_rate(rs))) / (3 / 2 * phys.nH * rs**3 * (1 + chi + xe)))
def dyHII_dz(yHII, yHeII, yHeIII, log_T_m, rs): T_m = np.exp(log_T_m) if 1 - xHII(yHII) < 1e-6 and rs < 100: # At this point, leave at 1 - 1e-6 return 0 # if yHII > 14. or yHII < -14.: # # Stops the solver from wandering too far. # return 0 if xHeII(yHeII) > 0.99 * chi and rs > 1500: # This is prior to helium recombination. # Assume H completely ionized. return 0 if helium_TLA and xHII(yHII) > 0.999 and rs > 1500: # Use the Saha value. return 2 * np.cosh(yHII)**2 * phys.d_xe_Saha_dz(rs, 'HI') if not helium_TLA and xHII(yHII) > 0.99 and rs > 1500: # Use the Saha value. return 2 * np.cosh(yHII)**2 * phys.d_xe_Saha_dz(rs, 'HI') xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII) ne = xe * nH xHI = 1 - xHII(yHII) xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII) return 2 * np.cosh(yHII)**2 * phys.dtdz(rs) * ( # Recombination processes. # Boltzmann factor is T_r, agrees with HyREC paper. -phys.peebles_C(xHII(yHII), rs) * (phys.alpha_recomb(T_m, 'HI') * xHII(yHII) * xe * nH - 4 * phys.beta_ion(phys.TCMB(rs), 'HI') * xHI * np.exp(-phys.lya_eng / phys.TCMB(rs))) # DM injection. Note that C = 1 at late times. + _f_H_ion(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate / (phys.rydberg * nH) + (1 - phys.peebles_C(xHII(yHII), rs)) * (_f_H_exc(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate / (phys.lya_eng * nH)))
def ics_spec( eleckineng, photeng, T, as_pairs=False, inf_upp_bound=True, thomson_tf=None, rel_tf=None, T_ref=None ): """ ICS spectrum of secondary photons. Switches between `thomson_spec` and `rel_spec`. Parameters ---------- eleckineng : ndarray Incoming electron energy. photeng : ndarray Outgoing photon energy. T : float CMB temperature. as_pairs : bool, optional If True, treats eleckineng and photeng as a paired list: produces eleckineng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleckineng, returning an array of length eleckineng.size*photeng.size. inf_upp_bound : bool If True, calculates the approximate relativistic spectrum that is used for fast interpolation over different values of T. See Notes for more details. Default is True. thomson_tf : TransFuncAtRedshift, optional Reference Thomson ICS transfer function. If specified, calculation is done by interpolating over the transfer function. rel_tf : TransFuncAtRedshift, optional Reference relativistic ICS transfer function. If specified, calculation is done by interpolating over the transfer function. T_ref : float, optional The reference temperature at which the reference transfer functions is evaluated. If not specified, defaults to phys.TCMB(400). Returns ------- TransFuncAtRedshift dN/(dt dE) of the outgoing photons, dt = 1 s, with `self.in_eng = eleckineng` and `self.eng = photeng`. `self.rs` is determined from `T`, and `self.dlnz` is normalized to 1 second. Notes ----- Insert note on the suitability of the method. """ if not inf_upp_bound and (thomson_tf is not None or rel_tf is not None): raise ValueError('inf_upp_bound must be True in order to use an interpolation over reference transfer functions.') gamma = eleckineng/phys.me + 1 eleceng = eleckineng + phys.me if as_pairs: if eleceng.size != photeng.size: raise TypeError('Photon and electron energy arrays must have the same length for pairwise computation.') gamma_mask = gamma eleceng_mask = eleceng eleckineng_mask = eleckineng photeng_mask = photeng spec = np.zeros(gamma) else: gamma_mask = np.outer(gamma, np.ones(photeng.size)) eleceng_mask = np.outer(eleceng, np.ones(photeng.size)) eleckineng_mask = np.outer(eleckineng, np.ones(photeng.size)) photeng_mask = np.outer(np.ones(eleceng.size), photeng) spec = np.zeros((eleceng.size, photeng.size), dtype='float128') rel_bound = 20 rel = (gamma_mask > rel_bound) if T_ref is None: T_ref = phys.TCMB(400) y = T/T_ref if rel_tf != None: if as_pairs: raise TypeError('When reading from file, the keyword as_pairs is not supported.') # If the electron energy at which interpolation is to be taken is outside rel_tf, then an error should be returned, since the file has not gone up to high enough energies. # Note relativistic spectrum is indexed by TOTAL electron energy. # rel_tf = rel_tf.at_in_eng(y*eleceng[gamma > rel_bound]) # If the photon energy at which interpolation is to be taken is outside rel_tf, then for large photon energies, we set it to zero, since the spectrum should already be zero long before. If it is below, nan is returned, and the results should not be used. rel_tf_interp = np.transpose( rel_tf.interp_func( np.log(y*eleceng[gamma > rel_bound]), np.log(y*photeng) ) ) spec[rel] = y**4*rel_tf_interp.flatten() else: spec[rel] = rel_spec( eleceng_mask[rel], photeng_mask[rel], T, inf_upp_bound=inf_upp_bound, as_pairs=True ) if thomson_tf != None: thomson_tf_interp = np.transpose( thomson_tf.interp_func( np.log(eleckineng[gamma <= rel_bound]), np.log(photeng/y) ) ) spec[~rel] = y**2*thomson_tf_interp.flatten() else: spec[~rel] = thomson_spec( eleckineng_mask[~rel], photeng_mask[~rel], T, as_pairs=True ) # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. Non-zero to take log interpolations later. spec[spec < 1e-100] = 1e-100 if as_pairs: return spec else: rs = T/phys.TCMB(1) dlnz = -1./(phys.dtdz(rs)*rs) return TransFuncAtRedshift( spec, in_eng = eleckineng, eng = photeng, rs = np.ones_like(eleckineng)*rs, dlnz=dlnz, spec_type = 'dNdE' )
def rel_spec(eleceng, photeng, T, inf_upp_bound=False, as_pairs=False): """ Relativistic ICS spectrum of secondary photons. Parameters ---------- eleceng : ndarray Incoming electron energy. photeng : ndarray Outgoing photon energy. T : float CMB temperature. inf_upp_bound : bool If True, calculates the approximate spectrum that is used for fast interpolation over different values of T. See Notes for more details. Default is False. as_pairs : bool If true, treats eleceng and photeng as a paired list: produces eleceng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleceng, returning an array of length eleceng.size*photeng.size. Returns ------- TransFuncAtRedshift or ndarray dN/(dt dE) of the outgoing photons (dt = 1 s). If as_pairs == False, returns a TransFuncAtRedshift, with abscissa given by (eleceng, photeng). Otherwise, returns an ndarray, with abscissa given by each pair of (eleceng, photeng). Notes ----- This function accepts the *energy* of the electron as one of the arguments and not the kinetic energy, unlike the other related ICS functions. The flag ``inf_upp_bound`` determines whether an approximation is taken that only gets the shape of the spectrum correct for :math:`E_{\\gamma,\\text{final}} \\gtrsim T_\\text{CMB}`. This is sufficient from an energy conservation perspective, and is used for building a table that can be interpolated over different values of T quickly. If ``inf_upp_bound == False``, the spectrum up to :math:`\\mathcal{O}(1/\\gamma^2)` corrections is given. This is a combination of the spectrum derived in Eq. (2.48) of Ref. [1]_ and Eq. (9) of Ref. [2]_, which assumes that electrons only lose energy, and Eq. (8) of Ref. [2]_, which contains the spectrum of photons produced electrons getting upscattered. See Also --------- :function:`.rel_spec_Jones_corr` """ print('Initializing...') gamma = eleceng/phys.me if as_pairs: if eleceng.size != photeng.size: raise TypeError('Photon and electron energy arrays must have the same length for pairwise computation.') Gamma_eps_q = ( np.divide( photeng/eleceng, 1 - photeng/eleceng, out = np.zeros_like(photeng), where = 1 - photeng/eleceng != 0 ) ) B = phys.me/(4*gamma)*Gamma_eps_q lowlim = B/T if inf_upp_bound: upplim = np.inf*np.ones_like(gamma) else: upplim = photeng/T # upplim = 4*(gamma**2)*B/T else: photeng_to_eleceng = np.outer(1/eleceng, photeng) Gamma_eps_q = ( np.divide( photeng_to_eleceng, 1 - photeng_to_eleceng, out = np.zeros_like(photeng_to_eleceng), where = 1 - photeng_to_eleceng != 0 ) ) B = np.transpose( phys.me/(4*gamma)*np.transpose(Gamma_eps_q) ) lowlim = B/T if inf_upp_bound: upplim = np.inf*np.ones_like(photeng_to_eleceng) else: upplim = np.outer(np.ones_like(eleceng), photeng)/T # upplim = np.transpose( # 4*gamma**2*np.transpose(B)/T # ) spec = np.zeros_like(Gamma_eps_q) F1_int = np.zeros_like(Gamma_eps_q) F0_int = np.zeros_like(Gamma_eps_q) F_inv_int = np.zeros_like(Gamma_eps_q) F_log_int = np.zeros_like(Gamma_eps_q) term_1 = np.zeros_like(Gamma_eps_q) term_2 = np.zeros_like(Gamma_eps_q) term_3 = np.zeros_like(Gamma_eps_q) term_4 = np.zeros_like(Gamma_eps_q) good = (lowlim > 0) Q = np.zeros_like(Gamma_eps_q) Q[good] = (1/2)*Gamma_eps_q[good]**2/(1 + Gamma_eps_q[good]) prefac = np.float128( 6*np.pi*phys.thomson_xsec*phys.c*T/(gamma**2) /(phys.ele_compton*phys.me)**3 ) print('Computing series 1/4...') F1_int[good] = F1(lowlim[good], upplim[good]) print('Computing series 2/4...') F0_int[good] = F0(lowlim[good], upplim[good]) print('Computing series 3/4...') F_inv_int[good] = F_inv(lowlim[good], upplim[good])[0] print('Computing series 4/4...') F_log_int[good] = F_log(lowlim[good], upplim[good])[0] term_1[good] = (1 + Q[good])*T*F1_int[good] term_2[good] = ( (1 + 2*np.log(B[good]/T) - Q[good])*B[good]*F0_int[good] ) term_3[good] = -2*B[good]*F_log_int[good] term_4[good] = -2*B[good]**2/T*F_inv_int[good] testing = False if testing: print('***** Diagnostics *****') print('gamma: ', gamma) print('lowlim: ', lowlim) print('lowlim*T: ', lowlim*T) print('upplim: ', upplim) print('upplim*T: ', upplim*T) print('Gamma_eps_q: ', Gamma_eps_q) print('Q: ', Q) print('B: ', B) print('***** Integrals *****') print('term_1: ', term_1) print('term_2: ', term_2) print('term_3: ', term_3) print('term_4: ', term_4) print('Sum of terms: ', term_1+term_2+term_3+term_4) print('Final answer: ', np.transpose( prefac*np.transpose( term_1 + term_2 + term_3 + term_4 ) ) ) print('***** End Diagnostics *****') print('Relativistic Computation Complete!') spec[good] = ( term_1[good] + term_2[good] + term_3[good] + term_4[good] ) spec = np.transpose(prefac*np.transpose(spec)) # Get the downscattering correction if requested. if not inf_upp_bound: downscatter_spec = rel_spec_Jones_corr( eleceng, photeng, T, as_pairs=as_pairs ) spec += downscatter_spec # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. spec[spec < 1e-100] = 0. if as_pairs: return spec else: rs = T/phys.TCMB(1) dlnz = -1./(phys.dtdz(rs)*rs) spec_arr = [ Spectrum(photeng, s, rs=rs, in_eng=in_eng) for s, in_eng in zip(spec, eleceng) ] spec_tf = TransFuncAtRedshift( spec_arr, dlnz=dlnz, in_eng = eleceng, eng = photeng, with_interp_func = True ) return spec_tf
def thomson_spec(eleckineng, photeng, T, as_pairs=False): """ Thomson ICS spectrum of secondary photons. Switches between `thomson_spec_diff` and `thomson_spec_series`. Parameters ---------- eleckineng : ndarray Incoming electron kinetic energy. photeng : ndarray Outgoing photon energy. T : float CMB temperature. as_pairs : bool If true, treats eleckineng and photeng as a paired list: produces eleckineng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleckineng, returning an array of length eleckineng.size*photeng.size. Returns ------- TransFuncAtRedshift or ndarray dN/(dt dE) of the outgoing photons (dt = 1 s). If as_pairs == False, returns a TransFuncAtRedshift, with abscissa given by (eleckineng, photeng). Otherwise, returns an ndarray, with abscissa given by each pair of (eleckineng, photeng). Notes ----- Insert note on the suitability of the method. """ print('Initializing...') gamma = eleckineng/phys.me + 1 # Most accurate way of finding beta when beta is small, I think. beta = np.sqrt(eleckineng/phys.me*(gamma+1)/gamma**2) eta = photeng/T # Masks, dimensions (eleckineng, photeng) if as_pairs == False. if as_pairs: if eleckineng.size != photeng.size: raise TypeError('Photon and electron energy arrays must have the same length for pairwise computation.') beta_mask = beta eta_mask = eta eleckineng_mask = eleckineng photeng_mask = photeng else: beta_mask = np.outer(beta, np.ones(eta.size)) eta_mask = np.outer(np.ones(beta.size), eta) eleckineng_mask = np.outer(eleckineng, np.ones(photeng.size)) photeng_mask = np.outer(np.ones(eleckineng.size), photeng) # Boolean arrays. Depending on as_pairs, can be 1- or 2-D. beta_small = (beta_mask < 0.01) eta_small = (eta_mask < 0.1/beta_mask) where_diff = (beta_small & eta_small) testing = False if testing: print('where_diff on (eleckineng, photeng) grid: ') print(where_diff) if as_pairs: spec = np.zeros_like(eleckineng) epsrel = np.zeros_like(eleckineng) else: spec = np.zeros((eleckineng.size, photeng.size), dtype='float128') epsrel = np.zeros((eleckineng.size, photeng.size), dtype='float128') spec[where_diff], err_with_diff = thomson_spec_diff( eleckineng_mask[where_diff], photeng_mask[where_diff], T, as_pairs=True ) epsrel[where_diff] = np.abs( np.divide( err_with_diff, spec[where_diff], out = np.zeros_like(err_with_diff), where = (spec[where_diff] != 0) ) ) if testing: print('spec from thomson_spec_diff: ') print(spec) print('epsrel from thomson_spec_diff: ') print(epsrel) where_series = (~where_diff) | (epsrel > 1e-3) if testing: print('where_series on (eleckineng, photeng) grid: ') print(where_series) spec[where_series] = thomson_spec_series( eleckineng_mask[where_series], photeng_mask[where_series], T, as_pairs=True ) if testing: spec_with_series = np.array(spec) spec_with_series[~where_series] = 0 print('spec from thomson_spec_series: ') print(spec_with_series) print('*********************') print('Final Result: ') print(spec) print('########### Spectrum computed! ###########') # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. Non-zero so that we can take log interpolations later. spec[spec < 1e-100] = 0. if as_pairs: return spec else: rs = T/phys.TCMB(1) dlnz = -1./(phys.dtdz(rs)*rs) spec_arr = [ Spectrum(photeng, s, rs=rs, in_eng=in_eng) for s, in_eng in zip(spec, eleckineng) ] # Injection energy is kinetic energy of the electron. spec_tf = TransFuncAtRedshift( spec_arr, dlnz=dlnz, in_eng = eleckineng, eng = photeng, with_interp_func = True ) return spec_tf
def engloss_spec( eleckineng, delta, T, as_pairs=False, thomson_only=False, thomson_tf=None, rel_tf=None, ): """ Thomson ICS scattered electron energy loss spectrum. Switches between :func:`.engloss_spec_series` and :func:`.engloss_spec_diff` in the Thomson regime. Also switches between Thomson and relativistic regimes automatically. Parameters ---------- eleckineng : ndarray Incoming electron kinetic energy. delta : ndarray Energy gained by photon after upscattering (only positive values). T : float CMB temperature. as_pairs : bool, optional If true, treats eleckineng and photeng as a paired list: produces eleckineng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleckineng, returning an array of length eleckineng.size*photeng.size. thomson_only : bool, optional If true, only returns the Thomson energy loss spectrum, and never switches to the relativistic case. thomson_tf : TransFuncAtRedshift, optional Reference Thomson energy loss ICS spectrum. If specified, calculation is done by interpolating over the transfer function. rel_tf : TransFuncAtRedshift, optional Reference relativistic energy loss ICS spectrum. If specified, calculation is done by interpolating over the transfer function. Returns ------- TransFuncAtRedshift or ndarray dN/(dt d Delta) of the outgoing photons (dt = 1 s). If as_pairs == False, returns a TransFuncAtRedshift, with abscissa given by (eleckineng, delta). Otherwise, returns an ndarray, with abscissa given by each pair of (eleckineng, delta). """ gamma = eleckineng/phys.me + 1 eleceng = eleckineng + phys.me beta = np.sqrt(eleckineng/phys.me*(gamma+1)/gamma**2) eta = delta/T # where to switch between Thomson and relativistic treatments. if thomson_only: rel_bound = np.inf else: rel_bound = 20 # 2D masks have dimensions (eleceng, delta). if as_pairs: if eleckineng.size != delta.size: raise TypeError('delta and electron energy arrays must have the same length for pairwise computation.') gamma_mask = gamma beta_mask = beta eleckineng_mask = eleckineng eleceng_mask = eleceng delta_mask = delta spec = np.zeros_like(gamma) else: gamma_mask = np.outer(gamma, np.ones_like(eta)) beta_mask = np.outer(beta, np.ones_like(eta)) eleckineng_mask = np.outer(eleckineng, np.ones_like(eta)) eleceng_mask = np.outer(eleceng, np.ones_like(eta)) delta_mask = np.outer(np.ones_like(eleckineng), delta) spec = np.zeros( (eleckineng.size, delta.size), dtype='float128' ) beta_small = beta_mask < 0.1 rel = gamma_mask > rel_bound y = T/phys.TCMB(400) if not thomson_only: if rel_tf != None: if as_pairs: raise TypeError('When reading from file, the keyword as_pairs is not supported.') # If the electron energy at which interpolation is to be taken is outside rel_tf, then an error should be returned, since the file has not gone up to high enough energies. #rel_tf = rel_tf.at_in_eng(y*eleceng[gamma > rel_bound]) # If the photon energy at which interpolation is to be taken is outside rel_tf, then for large photon energies, we set it to zero, since the spectrum should already be zero long before. If it is below, nan is returned, and the results should not be used. rel_tf_interp = np.transpose( rel_tf.interp_func( np.log(y*eleceng[gamma > rel_bound]), np.log(y*delta) ) ) spec[rel] = y**4*rel_tf_interp.flatten() else: print( '###### RELATIVISTIC ENERGY LOSS SPECTRUM ######' ) spec[rel] = ics_spectrum.rel_spec( eleceng_mask[rel], delta_mask[rel], T, inf_upp_bound=True, as_pairs=True ) print('###### COMPLETE! ######') if thomson_tf != None: thomson_tf_interp = np.transpose( thomson_tf.interp_func( np.log(eleckineng[gamma <= rel_bound]), np.log(delta/y) ) ) spec[~rel] = y**2*thomson_tf_interp.flatten() else: print('###### THOMSON ENERGY LOSS SPECTRUM ######') # beta_small obviously doesn't intersect with rel. spec[beta_small] = engloss_spec_diff( eleckineng_mask[beta_small], delta_mask[beta_small], T, as_pairs=True ) spec[~beta_small & ~rel] = engloss_spec_series( eleckineng_mask[~beta_small & ~rel], delta_mask[~beta_small & ~rel], T, as_pairs=True ) print('###### COMPLETE! ######') # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. Non-zero to take log interpolation later. spec[spec < 1e-100] = 1e-100 if as_pairs: return spec else: rs = T/phys.TCMB(1) dlnz = -1./(phys.dtdz(rs)*rs) return TransFuncAtRedshift( spec, in_eng = eleckineng, eng = delta, rs = np.ones_like(eleckineng)*rs, dlnz=dlnz, spec_type = 'dNdE', with_interp_func=True )