def __getitem__(self, key): if np.issubdtype(type(key), np.int64): out_spec = Spectrum(self.eng, self._grid_vals[key], in_eng=self._in_eng[key], rs=self._rs[key], spec_type=self.spec_type) if self.N_underflow.size > 0 and self.eng_underflow.size > 0: out_spec.underflow['N'] = self.N_underflow[key] out_spec.underflow['eng'] = self.eng_underflow[key] return out_spec elif isinstance(key, slice): data_arr = self._grid_vals[key] in_eng_arr = in_eng rs_arr = rs N_underflow_arr = self._N_underflow[key] eng_underflow_arr = self._eng_underflow[key] out_spec_list = [ Spectrum(self.eng, data, in_eng, rs) for (spec, in_eng, rs) in zip(data_arr, in_eng_arr, rs_arr) ] for (spec, N, eng) in zip(out_spec_list, N_underflow_arr, eng_underflow_arr): spec.underflow['N'] = N spec.underflow['eng'] = eng return out_spec_list else: raise TypeError("indexing is invalid.")
def sum_specs(self, weight=None): """Sums all of spectra with some weight. The weight is over each spectrum, and has the same length as `self.in_eng` and `self.rs`. Parameters ---------- weight : ndarray or Spectrum, optional The weight in each redshift bin, with weight of 1 for every bin if not specified. Returns ------- Spectrum A `Spectrum` of the weighted sum of the spectra. """ if weight is None: weight = np.ones_like(self.rs) if isinstance(weight, np.ndarray): new_data = np.dot(weight, self.grid_vals) return Spectrum(self.eng, new_data, spec_type=self.spec_type) elif isinstance(weight, Spectrum): if not np.array_equal(self.in_eng, weight.eng): raise TypeError('spectra.in_eng must equal weight.eng') # new_data = np.dot(weight._data, self.grid_vals) # Should always take the dot with type 'N'. new_data = np.dot(weight.N, self.grid_vals) return Spectrum(self.eng, new_data, spec_type=weight.spec_type) else: raise TypeError('weight must be an ndarray or Spectrum.')
def redshift(self, rs_arr): """ Redshifts the stored spectra. Parameters ---------- rs_arr : ndarray Array of redshifts (1+z) to redshift each spectrum to. Returns ------- None Examples -------- >>> from darkhistory.spec.spectrum import Spectrum >>> eng = np.array([1, 10, 100, 1000]) >>> spec_arr = [Spectrum(eng, np.ones(4)*i, rs=100, spec_type='N') for i in np.arange(4)] >>> test_spectra = Spectra(spec_arr) >>> test_spectra.redshift(np.array([0.01, 0.1, 1, 10])) >>> print(test_spectra.grid_vals) [[0. 0. 0. 0.] [1. 0. 0. 0.] [2. 2. 0. 0.] [3. 3. 3. 0.]] >>> print(test_spectra.N_underflow) [0. 3. 4. 3.] >>> print(test_spectra.eng_underflow) [0. 0.111 0.22 0.3 ] """ if rs_arr.size != self.rs.size: raise TypeError( 'rs_arr must have the same size as the number of Spectrum objects stored.' ) for i, (rs, new_rs, in_eng) in enumerate(zip(self.rs, rs_arr, self.in_eng)): spec = Spectrum(self.eng, self.grid_vals[i], rs=rs, in_eng=in_eng, spec_type=self.spec_type) spec.redshift(new_rs) self._grid_vals[i] = spec._data self._N_underflow[i] += spec.underflow['N'] self._eng_underflow[i] += spec.underflow['eng'] self._rs = rs_arr
def test_redshift(): eng = np.array([3, 10, 29, 3000]) N = np.array([0.2, 4.6, 3.38e5, 201.041]) spec = Spectrum(eng, N, spec_type='N', rs=2302.3) orig_totN = spec.totN() orig_toteng = spec.toteng() spec.redshift(842.10) assert spec.totN() == approx(orig_totN) assert spec.toteng() == approx(orig_toteng * 842.10 / 2302.3)
def __iter__(self): return iter([ Spectrum(self.eng, spec, in_eng=in_eng, rs=rs, spec_type=self.spec_type) for spec, in_eng, rs in zip(self.grid_vals, self.in_eng, self.rs) ])
def coll_ion_sec_elec_spec(in_eng, eng, species=None): """ Secondary electron spectrum after collisional ionization. See 0910.4410. Parameters ---------- in_eng : float The incoming electron energy. eng : ndarray Abscissa of *kinetic* energies. species : {'HI', 'HeI', 'HeII'} Species of interest. Returns ------- ndarray Secondary electron spectrum. Total number of electrons = 2. Notes ----- Includes both the freed and initial electrons. Conservation of energy is not enforced, but number of electrons is. """ from darkhistory.spec.spectrum import Spectrum from darkhistory.spec import spectools if species == 'HI': eps_i = 8. ion_pot = rydberg elif species == 'HeI': eps_i = 15.8 ion_pot = He_ion_eng elif species == 'HeII': eps_i = 32.6 ion_pot = 4 * rydberg else: raise TypeError('invalid species.') if np.isscalar(in_eng): low_eng_elec_dNdE = 1 / (1 + (eng / eps_i)**2.1) # This spectrum describes the lower energy electron only. low_eng_elec_dNdE[eng >= (in_eng - ion_pot) / 2] = 0 # Normalize the spectrum to one electron. low_eng_elec_spec = Spectrum(eng, low_eng_elec_dNdE) if np.sum(low_eng_elec_dNdE) == 0: # Either in_eng < in_pot, or the lowest bin lies # above the halfway point, (in_eng - ion_pot)/2. # Add to the lowest bin. return np.zeros_like(eng) low_eng_elec_spec /= low_eng_elec_spec.totN() in_eng = np.array([in_eng]) low_eng_elec_N = np.outer(np.ones_like(in_eng), low_eng_elec_spec.N) high_eng_elec_N = spectools.engloss_rebin_fast(in_eng, eng + ion_pot, low_eng_elec_N, eng) return np.squeeze(low_eng_elec_N + high_eng_elec_N) else: from darkhistory.spec.spectra import Spectra in_eng_mask = np.outer(in_eng, np.ones_like(eng)) eng_mask = np.outer(np.ones_like(in_eng), eng) low_eng_elec_dNdE = np.outer(np.ones_like(in_eng), 1 / (1 + (eng / eps_i)**2.1)) low_eng_elec_dNdE[eng_mask >= (in_eng_mask - ion_pot) / 2] = 0 # Normalize the spectrum to one electron. low_eng_elec_spec = Spectra(low_eng_elec_dNdE, eng=eng, in_eng=in_eng) totN_arr = low_eng_elec_spec.totN() # Avoids divide by zero errors. totN_arr[totN_arr == 0] = np.inf low_eng_elec_spec /= totN_arr if low_eng_elec_spec.spec_type == 'dNdE': low_eng_elec_spec.switch_spec_type() low_eng_elec_N = low_eng_elec_spec.grid_vals high_eng_elec_N = spectools.engloss_rebin_fast(in_eng, eng + ion_pot, low_eng_elec_N, eng) return low_eng_elec_N + high_eng_elec_N
def get_elec_cooling_tf( eleceng, photeng, rs, xHII, xHeII=0, raw_thomson_tf=None, raw_rel_tf=None, raw_engloss_tf=None, coll_ion_sec_elec_specs=None, coll_exc_sec_elec_specs=None, ics_engloss_data=None, check_conservation_eng = False, verbose=False ): """Transfer functions for complete electron cooling through inverse Compton scattering (ICS) and atomic processes. Parameters ---------- eleceng : ndarray, shape (m, ) The electron kinetic energy abscissa. photeng : ndarray The photon energy abscissa. rs : float The redshift (1+z). xHII : float Ionized hydrogen fraction, nHII/nH. xHeII : float, optional Singly-ionized helium fraction, nHe+/nH. Default is 0. raw_thomson_tf : TransFuncAtRedshift, optional Thomson ICS scattered photon spectrum transfer function. If None, uses the default transfer function. Default is None. raw_rel_tf : TransFuncAtRedshift, optional Relativistic ICS scattered photon spectrum transfer function. If None, uses the default transfer function. Default is None. raw_engloss_tf : TransFuncAtRedshift, optional Thomson ICS scattered electron net energy loss transfer function. If None, uses the default transfer function. Default is None. coll_ion_sec_elec_specs : tuple of 3 ndarrays, shapes (m, m), optional Normalized collisional ionization secondary electron spectra, order HI, HeI, HeII, indexed by injected electron energy by outgoing electron energy. If None, the function calculates this. Default is None. coll_exc_sec_elec_specs : tuple of 3 ndarray, shapes (m, m), optional Normalized collisional excitation secondary electron spectra, order HI, HeI, HeII, indexed by injected electron energy by outgoing electron energy. If None, the function calculates this. Default is None. ics_engloss_data : EnglossRebinData An `EnglossRebinData` object which stores rebinning information (based on ``eleceng`` and ``photeng``) for speed. Default is None. check_conservation_eng : bool If True, lower=True, checks for energy conservation. Default is False. verbose : bool If True, prints energy conservation checks. Default is False. Returns ------- tuple Transfer functions for electron cooling deposition and spectra. See Also --------- :class:`.TransFuncAtRedshift` :class:`.EnglossRebinData` :mod:`.ics` Notes ----- The entries of the output tuple are (see Sec IIIC of the paper): 0. The secondary propagating photon transfer function :math:`\\overline{\\mathsf{T}}_\\gamma`; 1. The low-energy electron transfer function :math:`\\overline{\\mathsf{T}}_e`; 2. Energy deposited into ionization :math:`\\overline{\\mathbf{R}}_\\text{ion}`; 3. Energy deposited into excitation :math:`\\overline{\\mathbf{R}}_\\text{exc}`; 4. Energy deposited into heating :math:`\\overline{\\mathbf{R}}_\\text{heat}`; 5. Upscattered CMB photon total energy :math:`\\overline{\\mathbf{R}}_\\text{CMB}`, and 6. Numerical error away from energy conservation. Items 2--5 are vectors that when dotted into an electron spectrum with abscissa ``eleceng``, return the energy deposited/CMB energy upscattered for that spectrum. Items 0--1 are :class:`.TransFuncAtRedshift` objects. For each of these objects ``tf`` and a given electron spectrum ``elec_spec``, ``tf.sum_specs(elec_spec)`` returns the propagating photon/low-energy electron spectrum after cooling. The default version of the three ICS transfer functions that are required by this function is provided in :mod:`.tf_data`. """ # Use default ICS transfer functions if not specified. ics_tf = load_data('ics_tf') raw_thomson_tf = ics_tf['thomson'] raw_rel_tf = ics_tf['rel'] raw_engloss_tf = ics_tf['engloss'] if coll_ion_sec_elec_specs is None: # Compute the (normalized) collisional ionization spectra. coll_ion_sec_elec_specs = ( phys.coll_ion_sec_elec_spec(eleceng, eleceng, species='HI'), phys.coll_ion_sec_elec_spec(eleceng, eleceng, species='HeI'), phys.coll_ion_sec_elec_spec(eleceng, eleceng, species='HeII') ) if coll_exc_sec_elec_specs is None: # Compute the (normalized) collisional excitation spectra. id_mat = np.identity(eleceng.size) # Electron with energy eleceng produces a spectrum with one particle # of energy eleceng - phys.lya.eng. Similar for helium. coll_exc_sec_elec_tf_HI = tf.TransFuncAtRedshift( np.squeeze(id_mat[:, np.where(eleceng > phys.lya_eng)]), in_eng = eleceng, rs = -1*np.ones_like(eleceng), eng = eleceng[eleceng > phys.lya_eng] - phys.lya_eng, dlnz = -1, spec_type = 'N' ) coll_exc_sec_elec_tf_HeI = tf.TransFuncAtRedshift( np.squeeze( id_mat[:, np.where(eleceng > phys.He_exc_eng['23s'])] ), in_eng = eleceng, rs = -1*np.ones_like(eleceng), eng = ( eleceng[eleceng > phys.He_exc_eng['23s']] - phys.He_exc_eng['23s'] ), dlnz = -1, spec_type = 'N' ) coll_exc_sec_elec_tf_HeII = tf.TransFuncAtRedshift( np.squeeze(id_mat[:, np.where(eleceng > 4*phys.lya_eng)]), in_eng = eleceng, rs = -1*np.ones_like(eleceng), eng = eleceng[eleceng > 4*phys.lya_eng] - 4*phys.lya_eng, dlnz = -1, spec_type = 'N' ) # Rebin the data so that the spectra stored above now have an abscissa # of eleceng again (instead of eleceng - phys.lya_eng for HI etc.) coll_exc_sec_elec_tf_HI.rebin(eleceng) coll_exc_sec_elec_tf_HeI.rebin(eleceng) coll_exc_sec_elec_tf_HeII.rebin(eleceng) # Put them in a tuple. coll_exc_sec_elec_specs = ( coll_exc_sec_elec_tf_HI.grid_vals, coll_exc_sec_elec_tf_HeI.grid_vals, coll_exc_sec_elec_tf_HeII.grid_vals ) # Set the electron fraction. xe = xHII + xHeII # v/c of electrons beta_ele = np.sqrt(1 - 1/(1 + eleceng/phys.me)**2) ##################################### # Inverse Compton ##################################### T = phys.TCMB(rs) # Photon transfer function for single primary electron single scattering. # This is dN/(dE dt), dt = 1 s. phot_ICS_tf = ics_spec( eleceng, photeng, T, thomson_tf = raw_thomson_tf, rel_tf = raw_rel_tf ) # Downcasting speeds up np.dot phot_ICS_tf._grid_vals = phot_ICS_tf.grid_vals.astype('float64') # Energy loss transfer function for single primary electron # single scattering. This is dN/(dE dt), dt = 1 s. engloss_ICS_tf = engloss_spec( eleceng, photeng, T, thomson_tf = raw_engloss_tf, rel_tf = raw_rel_tf ) # Downcasting speeds up np.dot engloss_ICS_tf._grid_vals = engloss_ICS_tf.grid_vals.astype('float64') # Switch the spectra type here to type 'N'. if phot_ICS_tf.spec_type == 'dNdE': phot_ICS_tf.switch_spec_type() if engloss_ICS_tf.spec_type == 'dNdE': engloss_ICS_tf.switch_spec_type() # Define some useful lengths. N_eleceng = eleceng.size N_photeng = photeng.size # Create the secondary electron transfer functions. # ICS transfer function. elec_ICS_tf = tf.TransFuncAtRedshift( np.zeros((N_eleceng, N_eleceng)), in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) if ics_engloss_data is not None: elec_ICS_tf._grid_vals = ics_engloss_data.rebin( engloss_ICS_tf.grid_vals ) else: elec_ICS_tf._grid_vals = spectools.engloss_rebin_fast( eleceng, photeng, engloss_ICS_tf.grid_vals, eleceng ) # Total upscattered photon energy. cont_loss_ICS_vec = np.zeros_like(eleceng) # Deposited energy, enforces energy conservation. deposited_ICS_vec = np.zeros_like(eleceng) ######################### # Collisional Excitation ######################### # Collisional excitation rates. rate_vec_exc_HI = ( (1 - xHII)*phys.nH*rs**3 * phys.coll_exc_xsec(eleceng, species='HI') * beta_ele * phys.c ) rate_vec_exc_HeI = ( (phys.nHe/phys.nH - xHeII)*phys.nH*rs**3 * phys.coll_exc_xsec(eleceng, species='HeI') * beta_ele * phys.c ) rate_vec_exc_HeII = ( xHeII*phys.nH*rs**3 * phys.coll_exc_xsec(eleceng, species='HeII') * beta_ele * phys.c ) # Normalized electron spectrum after excitation. elec_exc_HI_tf = tf.TransFuncAtRedshift( rate_vec_exc_HI[:, np.newaxis]*coll_exc_sec_elec_specs[0], in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) elec_exc_HeI_tf = tf.TransFuncAtRedshift( rate_vec_exc_HeI[:, np.newaxis]*coll_exc_sec_elec_specs[1], in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) elec_exc_HeII_tf = tf.TransFuncAtRedshift( rate_vec_exc_HeII[:, np.newaxis]*coll_exc_sec_elec_specs[2], in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) # Deposited energy for excitation. deposited_exc_vec = np.zeros_like(eleceng) ######################### # Collisional Ionization ######################### # Collisional ionization rates. rate_vec_ion_HI = ( (1 - xHII)*phys.nH*rs**3 * phys.coll_ion_xsec(eleceng, species='HI') * beta_ele * phys.c ) rate_vec_ion_HeI = ( (phys.nHe/phys.nH - xHeII)*phys.nH*rs**3 * phys.coll_ion_xsec(eleceng, species='HeI') * beta_ele * phys.c ) rate_vec_ion_HeII = ( xHeII*phys.nH*rs**3 * phys.coll_ion_xsec(eleceng, species='HeII') * beta_ele * phys.c ) # Normalized secondary electron spectra after ionization. elec_spec_ion_HI = ( rate_vec_ion_HI[:,np.newaxis] * coll_ion_sec_elec_specs[0] ) elec_spec_ion_HeI = ( rate_vec_ion_HeI[:,np.newaxis] * coll_ion_sec_elec_specs[1] ) elec_spec_ion_HeII = ( rate_vec_ion_HeII[:,np.newaxis] * coll_ion_sec_elec_specs[2] ) # Construct TransFuncAtRedshift objects. elec_ion_HI_tf = tf.TransFuncAtRedshift( elec_spec_ion_HI, in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) elec_ion_HeI_tf = tf.TransFuncAtRedshift( elec_spec_ion_HeI, in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) elec_ion_HeII_tf = tf.TransFuncAtRedshift( elec_spec_ion_HeII, in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) # Deposited energy for ionization. deposited_ion_vec = np.zeros_like(eleceng) ############################################# # Heating ############################################# dE_heat_dt = phys.elec_heating_engloss_rate(eleceng, xe, rs) deposited_heat_vec = np.zeros_like(eleceng) # new_eleceng = eleceng - dE_heat_dt # if not np.all(new_eleceng[1:] > eleceng[:-1]): # utils.compare_arr([new_eleceng, eleceng]) # raise ValueError('heating loss is too large: smaller time step required.') # # After the check above, we can define the spectra by # # manually assigning slightly less than 1 particle along # # diagonal, and a small amount in the bin below. # # N_n-1 E_n-1 + N_n E_n = E_n - dE_dt # # N_n-1 + N_n = 1 # # therefore, (1 - N_n) E_n-1 - (1 - N_n) E_n = - dE_dt # # i.e. N_n = 1 + dE_dt/(E_n-1 - E_n) elec_heat_spec_grid = np.identity(eleceng.size) elec_heat_spec_grid[0,0] -= dE_heat_dt[0]/eleceng[0] elec_heat_spec_grid[1:, 1:] += np.diag( dE_heat_dt[1:]/(eleceng[:-1] - eleceng[1:]) ) elec_heat_spec_grid[1:, :-1] -= np.diag( dE_heat_dt[1:]/(eleceng[:-1] - eleceng[1:]) ) ############################################# # Initialization of secondary spectra ############################################# # Low and high energy boundaries loweng = 3000 eleceng_high = eleceng[eleceng > loweng] eleceng_high_ind = np.arange(eleceng.size)[eleceng > loweng] eleceng_low = eleceng[eleceng <= loweng] eleceng_low_ind = np.arange(eleceng.size)[eleceng <= loweng] if eleceng_low.size == 0: raise TypeError('Energy abscissa must contain a low energy bin below 3 keV.') # Empty containers for quantities. # Final secondary photon spectrum. sec_phot_tf = tf.TransFuncAtRedshift( np.zeros((N_eleceng, N_photeng)), in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = photeng, dlnz = -1, spec_type = 'N' ) # Final secondary low energy electron spectrum. sec_lowengelec_tf = tf.TransFuncAtRedshift( np.zeros((N_eleceng, N_eleceng)), in_eng = eleceng, rs = rs*np.ones_like(eleceng), eng = eleceng, dlnz = -1, spec_type = 'N' ) # Continuum energy loss rate per electron, dU_CMB/dt. CMB_upscatter_eng_rate = phys.thomson_xsec*phys.c*phys.CMB_eng_density(T) # Secondary scattered electron spectrum. sec_elec_spec_N_arr = ( elec_ICS_tf.grid_vals + elec_exc_HI_tf.grid_vals + elec_exc_HeI_tf.grid_vals + elec_exc_HeII_tf.grid_vals + elec_ion_HI_tf.grid_vals + elec_ion_HeI_tf.grid_vals + elec_ion_HeII_tf.grid_vals + elec_heat_spec_grid ) # Secondary photon spectrum (from ICS). sec_phot_spec_N_arr = phot_ICS_tf.grid_vals # Deposited ICS array. deposited_ICS_eng_arr = ( np.sum(elec_ICS_tf.grid_vals, axis=1)*eleceng - np.dot(elec_ICS_tf.grid_vals, eleceng) - (np.dot(sec_phot_spec_N_arr, photeng) - CMB_upscatter_eng_rate) ) # Energy loss is not taken into account for eleceng > 20*phys.me deposited_ICS_eng_arr[eleceng > 20*phys.me - phys.me] -= ( CMB_upscatter_eng_rate ) # Continuum energy loss array. continuum_engloss_arr = CMB_upscatter_eng_rate*np.ones_like(eleceng) # Energy loss is not taken into account for eleceng > 20*phys.me continuum_engloss_arr[eleceng > 20*phys.me - phys.me] = 0 # Deposited excitation array. deposited_exc_eng_arr = ( phys.lya_eng*np.sum(elec_exc_HI_tf.grid_vals, axis=1) + phys.He_exc_eng['23s']*np.sum(elec_exc_HeI_tf.grid_vals, axis=1) + 4*phys.lya_eng*np.sum(elec_exc_HeII_tf.grid_vals, axis=1) ) # Deposited ionization array. deposited_ion_eng_arr = ( phys.rydberg*np.sum(elec_ion_HI_tf.grid_vals, axis=1)/2 + phys.He_ion_eng*np.sum(elec_ion_HeI_tf.grid_vals, axis=1)/2 + 4*phys.rydberg*np.sum(elec_ion_HeII_tf.grid_vals, axis=1)/2 ) # Deposited heating array. deposited_heat_eng_arr = dE_heat_dt # Remove self-scattering, re-normalize. np.fill_diagonal(sec_elec_spec_N_arr, 0) toteng_no_self_scatter_arr = ( np.dot(sec_elec_spec_N_arr, eleceng) + np.dot(sec_phot_spec_N_arr, photeng) - continuum_engloss_arr + deposited_ICS_eng_arr + deposited_exc_eng_arr + deposited_ion_eng_arr + deposited_heat_eng_arr ) fac_arr = eleceng/toteng_no_self_scatter_arr sec_elec_spec_N_arr *= fac_arr[:, np.newaxis] sec_phot_spec_N_arr *= fac_arr[:, np.newaxis] continuum_engloss_arr *= fac_arr deposited_ICS_eng_arr *= fac_arr deposited_exc_eng_arr *= fac_arr deposited_ion_eng_arr *= fac_arr deposited_heat_eng_arr *= fac_arr # Zero out deposition/ICS processes below loweng. deposited_ICS_eng_arr[eleceng < loweng] = 0 deposited_exc_eng_arr[eleceng < loweng] = 0 deposited_ion_eng_arr[eleceng < loweng] = 0 deposited_heat_eng_arr[eleceng < loweng] = 0 continuum_engloss_arr[eleceng < loweng] = 0 sec_phot_spec_N_arr[eleceng < loweng] = 0 # Scattered low energy and high energy electrons. # Needed for final low energy electron spectra. sec_lowengelec_N_arr = np.identity(eleceng.size) sec_lowengelec_N_arr[eleceng >= loweng] = 0 sec_lowengelec_N_arr[eleceng_high_ind[0]:, :eleceng_high_ind[0]] += sec_elec_spec_N_arr[eleceng_high_ind[0]:, :eleceng_high_ind[0]] sec_highengelec_N_arr = np.zeros_like(sec_elec_spec_N_arr) sec_highengelec_N_arr[:, eleceng_high_ind[0]:] = ( sec_elec_spec_N_arr[:, eleceng_high_ind[0]:] ) # T = E.T + Prompt deposited_ICS_vec = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, deposited_ICS_eng_arr, lower=True, check_finite=False ) deposited_exc_vec = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, deposited_exc_eng_arr, lower=True, check_finite=False ) deposited_ion_vec = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, deposited_ion_eng_arr, lower=True, check_finite=False ) deposited_heat_vec = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, deposited_heat_eng_arr, lower=True, check_finite=False ) cont_loss_ICS_vec = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, continuum_engloss_arr, lower=True, check_finite=False ) sec_phot_specs = solve_triangular( np.identity(eleceng.size) - sec_elec_spec_N_arr, sec_phot_spec_N_arr, lower=True, check_finite=False ) # Prompt: low energy e produced in secondary spectrum upon scattering (sec_lowengelec_N_arr). # T : high energy e produced (sec_highengelec_N_arr). sec_lowengelec_specs = solve_triangular( np.identity(eleceng.size) - sec_highengelec_N_arr, sec_lowengelec_N_arr, lower=True, check_finite=False ) # Subtract continuum from sec_phot_specs. After this point, # sec_phot_specs will contain the *distortions* to the CMB. # Normalized CMB spectrum. norm_CMB_spec = Spectrum( photeng, phys.CMB_spec(photeng, phys.TCMB(rs)), spec_type='dNdE' ) norm_CMB_spec /= norm_CMB_spec.toteng() # Get the CMB spectrum upscattered from cont_loss_ICS_vec. upscattered_CMB_grid = np.outer(cont_loss_ICS_vec, norm_CMB_spec.N) # Subtract this spectrum from sec_phot_specs to get the final # transfer function. sec_phot_tf._grid_vals = sec_phot_specs - upscattered_CMB_grid sec_lowengelec_tf._grid_vals = sec_lowengelec_specs # Conservation checks. failed_conservation_check = False if check_conservation_eng: conservation_check = ( eleceng - np.dot(sec_lowengelec_tf.grid_vals, eleceng) # + cont_loss_ICS_vec - np.dot(sec_phot_tf.grid_vals, photeng) - deposited_exc_vec - deposited_ion_vec - deposited_heat_vec ) if np.any(np.abs(conservation_check/eleceng) > 0.1): failed_conservation_check = True if verbose or failed_conservation_check: for i,eng in enumerate(eleceng): print('***************************************************') print('rs: ', rs) print('injected energy: ', eng) print( 'Fraction of Energy in low energy electrons: ', np.dot(sec_lowengelec_tf.grid_vals[i], eleceng)/eng ) # print('Energy in photons: ', # np.dot(sec_phot_tf.grid_vals[i], photeng) # ) # print('Continuum_engloss: ', cont_loss_ICS_vec[i]) print( 'Fraction of Energy in photons - Continuum: ', ( np.dot(sec_phot_tf.grid_vals[i], photeng)/eng # - cont_loss_ICS_vec[i] ) ) print( 'Fraction Deposited in ionization: ', deposited_ion_vec[i]/eng ) print( 'Fraction Deposited in excitation: ', deposited_exc_vec[i]/eng ) print( 'Fraction Deposited in heating: ', deposited_heat_vec[i]/eng ) print( 'Energy is conserved up to (%): ', conservation_check[i]/eng*100 ) print('Fraction Deposited in ICS (Numerical Error): ', deposited_ICS_vec[i]/eng ) print( 'Energy conservation with deposited (%): ', (conservation_check[i] - deposited_ICS_vec[i])/eng*100 ) print('***************************************************') if failed_conservation_check: raise RuntimeError('Conservation of energy failed.') return ( sec_phot_tf, sec_lowengelec_tf, deposited_ion_vec, deposited_exc_vec, deposited_heat_vec, cont_loss_ICS_vec, deposited_ICS_vec )
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 compute_fs(MEDEA_interp, elec_spec, phot_spec, x, dE_dVdt_inj, dt, highengdep, cmbloss=0, method='no_He', separate_higheng=True): """ Compute f(z) fractions for continuum photons, photoexcitation of HI, and photoionization of HI, HeI, HeII Given a spectrum of deposited electrons and photons, resolve their energy into H ionization, and ionization, H excitation, heating, and continuum photons in that order. Parameters ---------- phot_spec : Spectrum object spectrum of photons. Assumed to be in dNdE mode. spec.totN() should return number *per baryon*. elec_spec : Spectrum object spectrum of electrons. Assumed to be in dNdE mode. spec.totN() should return number *per baryon*. x : list of floats number of (HI, HeI, HeII) divided by nH at redshift photon_spectrum.rs dE_dVdt_inj : float DM energy injection rate, dE/dVdt injected. This is for unclustered DM (i.e. without structure formation). dt : float time in seconds over which these spectra were deposited. highengdep : list of floats total amount of energy deposited by high energy particles into {H_ionization, H_excitation, heating, continuum} per baryon per time, in that order. cmbloss : float Total amount of energy in upscattered photons that came from the CMB, per baryon per time, (1/n_B)dE/dVdt. Default is zero. method : {'no_He', 'He_recomb', 'He'} Method for evaluating helium ionization. * *'no_He'* -- all ionization assigned to hydrogen; * *'He_recomb'* -- all photoionized helium atoms recombine; and * *'He'* -- all photoionized helium atoms do not recombine. separate_higheng : bool, optional If True, returns separate high energy deposition. Returns ------- ndarray or tuple of ndarray f_c(z) for z within spec.rs +/- dt/2 The order of the channels is {H Ionization, He Ionization, H Excitation, Heating and Continuum} Notes ----- The CMB component hasn't been subtracted from the continuum photons yet Think about the exceptions that should be thrown (elec_spec.rs should equal phot_spec.rs) """ # np.array syntax below needed so that a fresh copy of eng and N are passed to the # constructor, instead of simply a reference. if method == 'no_He': ion_bounds = spectools.get_bounds_between( phot_spec.eng, phys.rydberg ) ion_engs = np.exp((np.log(ion_bounds[1:])+np.log(ion_bounds[:-1]))/2) ionized_elec = Spectrum( ion_engs, phot_spec.totN(bound_type="eng", bound_arr=ion_bounds), rs=phot_spec.rs, spec_type='N' ) new_eng = ion_engs - phys.rydberg ionized_elec.shift_eng(new_eng) # rebin so that ionized_elec may be added to elec_spec ionized_elec.rebin(elec_spec.eng) tmp_elec_spec = Spectrum( np.array(elec_spec.eng), np.array(elec_spec.N), rs=elec_spec.rs, spec_type='N' ) tmp_elec_spec.N += ionized_elec.N f_phot = lowE_photons.compute_fs( phot_spec, x, dE_dVdt_inj, dt, 'old' ) #print(phot_spec.rs, f_phot[0], phot_spec.toteng(), cmbloss, dE_dVdt_inj) f_elec = lowE_electrons.compute_fs( MEDEA_interp, tmp_elec_spec, 1-x[0], dE_dVdt_inj, dt ) # print('photons:', f_phot[2], f_phot[3]+f_phot[4], f_phot[1], 0, f_phot[0]) # print('electrons:', f_elec[2], f_elec[3], f_elec[1], f_elec[4], f_elec[0]) # f_low is {H ion, He ion, Lya Excitation, Heating, Continuum} f_low = np.array([ f_phot[2]+f_elec[2], f_phot[3]+f_phot[4]+f_elec[3], f_phot[1]+f_elec[1], f_elec[4], f_phot[0]+f_elec[0] - cmbloss*phys.nB*phot_spec.rs**3 / dE_dVdt_inj ]) f_high = np.array([ highengdep[0], 0, highengdep[1], highengdep[2], highengdep[3] ]) * phys.nB * phot_spec.rs**3 / dE_dVdt_inj if separate_higheng: return (f_low, f_high) else: return f_low + f_high elif method == 'He': # Neglect HeII photoionization. Photoionization rates. n = phys.nH*phot_spec.rs**3*x rates = np.array([ n[i]*phys.photo_ion_xsec(phot_spec.eng, chan) for i,chan in enumerate(['HI', 'HeI']) ]) norm_prob = np.sum(rates, axis=0) # Probability of photoionizing HI vs. HeI. prob = np.array([ np.divide( rate, norm_prob, out = np.zeros_like(phot_spec.eng), where=(phot_spec.eng > phys.rydberg) ) for rate in rates ]) # Spectra weighted by prob. phot_spec_HI = phot_spec*prob[0] phot_spec_HeI = phot_spec*prob[1] # Bin boundaries, including the lowest (13.6, 24.6) eV bin. ion_bounds_HI = spectools.get_bounds_between( phot_spec.eng, phys.rydberg ) ion_bounds_HeI = spectools.get_bounds_between( phot_spec.eng, phys.He_ion_eng ) # Bin centers. ion_engs_HI = np.exp( (np.log(ion_bounds_HI[1:]) + np.log(ion_bounds_HI[:-1]))/2 ) ion_engs_HeI = np.exp( (np.log(ion_bounds_HeI[1:]) + np.log(ion_bounds_HeI[:-1]))/2 ) # Spectrum object containing secondary electron # from ionization. ionized_elec_HI = Spectrum( ion_engs_HI, phot_spec_HI.totN(bound_type='eng', bound_arr=ion_bounds_HI), rs=phot_spec.rs, spec_type='N' ) ionized_elec_HeI = Spectrum( ion_engs_HeI, phot_spec_HeI.totN(bound_type='eng', bound_arr=ion_bounds_HeI), rs=phot_spec.rs, spec_type='N' ) # electron energy (photon energy - ionizing potential). new_eng_HI = ion_engs_HI - phys.rydberg new_eng_HeI = ion_engs_HeI - phys.He_ion_eng # change the Spectrum abscissa to the correct electron energy. ionized_elec_HI.shift_eng(new_eng_HI) ionized_elec_HeI.shift_eng(new_eng_HeI) # rebin so that ionized_elec may be added to elec_spec. ionized_elec_HI.rebin(elec_spec.eng) ionized_elec_HeI.rebin(elec_spec.eng) tmp_elec_spec = Spectrum( np.array(elec_spec.eng), np.array(elec_spec.N), rs=elec_spec.rs, spec_type='N' ) tmp_elec_spec.N += (ionized_elec_HI.N + ionized_elec_HeI.N) f_phot = lowE_photons.compute_fs( phot_spec, x, dE_dVdt_inj, dt, 'helium' ) f_elec = lowE_electrons.compute_fs( MEDEA_interp, tmp_elec_spec, 1-x[0], dE_dVdt_inj, dt ) # f_low is {H ion, He ion, Lya Excitation, Heating, Continuum} f_low = np.array([ f_phot[2]+f_elec[2], f_phot[3]+f_phot[4]+f_elec[3], f_phot[1]+f_elec[1], f_elec[4], f_phot[0]+f_elec[0] - cmbloss*phys.nB*phot_spec.rs**3 / dE_dVdt_inj ]) f_high = np.array([ highengdep[0], 0, highengdep[1], highengdep[2], highengdep[3] ]) * phys.nB * phot_spec.rs**3 / dE_dVdt_inj if separate_higheng: return (f_low, f_high) else: return f_low + f_high elif method == 'He_recomb': # Neglect HeII photoionization. Photoionization rates. n = phys.nH*phot_spec.rs**3*x rates = np.array([ n[i]*phys.photo_ion_xsec(phot_spec.eng, chan) for i,chan in enumerate(['HI', 'HeI']) ]) norm_prob = np.sum(rates, axis=0) # Probability of photoionizing HI vs. HeI. prob = np.array([ np.divide( rate, norm_prob, out = np.zeros_like(phot_spec.eng), where=(phot_spec.eng > phys.rydberg) ) for rate in rates ]) # Spectra weighted by prob. phot_spec_HI = phot_spec*prob[0] phot_spec_HeI = phot_spec*prob[1] # Bin boundaries, including the lowest (13.6, 24.6) eV bin. ion_bounds_HI = spectools.get_bounds_between( phot_spec.eng, phys.rydberg ) ion_bounds_HeI = spectools.get_bounds_between( phot_spec.eng, phys.He_ion_eng ) # Bin centers. ion_engs_HI = np.exp( (np.log(ion_bounds_HI[1:]) + np.log(ion_bounds_HI[:-1]))/2 ) ion_engs_HeI = np.exp( (np.log(ion_bounds_HeI[1:]) + np.log(ion_bounds_HeI[:-1]))/2 ) # Spectrum object containing secondary electron # from ionization. ionized_elec_HI = Spectrum( ion_engs_HI, phot_spec_HI.totN(bound_type='eng', bound_arr=ion_bounds_HI), rs=phot_spec.rs, spec_type='N' ) ionized_elec_HeI = Spectrum( ion_engs_HeI, phot_spec_HeI.totN(bound_type='eng', bound_arr=ion_bounds_HeI), rs=phot_spec.rs, spec_type='N' ) # electron energy (photon energy - ionizing potential). new_eng_HI = ion_engs_HI - phys.rydberg new_eng_HeI = ion_engs_HeI - phys.He_ion_eng # change the Spectrum abscissa to the correct electron energy. ionized_elec_HI.shift_eng(new_eng_HI) ionized_elec_HeI.shift_eng(new_eng_HeI) # rebin so that ionized_elec may be added to elec_spec. ionized_elec_HI.rebin(elec_spec.eng) ionized_elec_HeI.rebin(elec_spec.eng) tmp_elec_spec = Spectrum( np.array(elec_spec.eng), np.array(elec_spec.N), rs=elec_spec.rs, spec_type='N' ) tmp_elec_spec.N += (ionized_elec_HI.N + ionized_elec_HeI.N) # Every ionized helium recombines to produce an 11 eV electron. recomb_elec = spectools.rebin_N_arr( np.array([phot_spec_HeI.totN()]), np.array([phys.He_ion_eng - phys.rydberg]), elec_spec.eng ) tmp_elec_spec.N += recomb_elec.N # Every photon that photoionizes goes into hydrogen ionization now. # We can just use 'old' to do this computation. f_phot = lowE_photons.compute_fs( phot_spec, x, dE_dVdt_inj, dt, 'old' ) f_elec = lowE_electrons.compute_fs( MEDEA_interp, tmp_elec_spec, 1-x[0], dE_dVdt_inj, dt ) # f_low is {H ion, He ion, Lya Excitation, Heating, Continuum} f_low = np.array([ f_phot[2]+f_elec[2], f_phot[3]+f_phot[4]+f_elec[3], f_phot[1]+f_elec[1], f_elec[4], f_phot[0]+f_elec[0] - cmbloss*phys.nB*phot_spec.rs**3 / dE_dVdt_inj ]) f_high = np.array([ highengdep[0], 0, highengdep[1], highengdep[2], highengdep[3] ]) * phys.nB * phot_spec.rs**3 / dE_dVdt_inj if separate_higheng: return (f_low, f_high) else: return f_low + f_high else: raise TypeError('invalid method.')
def coarsen(self, dlnz_factor, delete_tfs=True, coarsen_type='prop', prop_transfunclist=None): """Coarsens the new transfer function with larger dlnz. This is obtained by multiplying the transfer function by itself several times, and removing intermediate transfer functions. Parameters ---------- dlnz_factor : int The factor to increase dlnz by. delete_tfs : bool If true, only retains transfer functions in tflist that have an index that is a multiple of dlnz_factor. coarsen_type : {'prop', 'dep'} The type of coarsening. Use 'prop' to coarsen by taking powers of the transfer function. Use 'dep' for deposition transfer functions, where coarsening is done by taking self * sum_i prop_tf**i. prop_tflist : TransferFuncList The transfer function for propagation, if the transfer function represents deposition. """ if self.tftype != 'rs': self.transpose() if coarsen_type == 'dep' and prop_transfunclist.tftype != 'rs': prop_transfunclist.transpose() if delete_tfs: new_tflist = [ self.tflist[i] for i in np.arange(0, len(self.tflist), dlnz_factor) ] else: # list() needed to create a new copy, not just point. new_tflist = list(self.tflist) self._tflist = [] if coarsen_type == 'dep': for i, (tfunc, prop_tfunc) in enumerate( zip(new_tflist, prop_transfunclist.tflist)): in_eng_arr = tfunc.in_eng if prop_tfunc.in_eng.size != prop_tfunc.eng.size: raise TypeError('propagation matrix is not square.') prop_part = np.zeros_like(prop_tfunc._grid_vals) for i in np.arange(dlnz_factor): prop_part += matrix_power(prop_tfunc._grid_vals, i) # We need to take eng x in_eng times the propagating part. # Need to return new_grid_val to in_eng x eng in the end. # new_grid_val = np.transpose( # np.dot(np.transpose(tfunc._grid_vals), prop_part) # ) new_grid_val = np.matmul(prop_part, tfunc._grid_vals) new_spec_arr = [ Spectrum(tfunc.eng, new_grid_val[i], spec_type=tfunc.spec_type, rs=tfunc.rs[0], in_eng=in_eng_arr[i]) for i in np.arange(in_eng_arr.size) ] self._tflist.append( tf.TransFuncAtRedshift(new_spec_arr, self.dlnz * dlnz_factor)) elif coarsen_type == 'prop': for (i, tfunc) in enumerate(new_tflist): in_eng_arr = tfunc.in_eng new_grid_val = matrix_power(tfunc._grid_vals, dlnz_factor) new_spec_arr = [ Spectrum(tfunc.eng, new_grid_val[i], spec_type=tfunc.spec_type, rs=tfunc.rs[0], in_eng=in_eng_arr[i]) for i in np.arange(in_eng_arr.size) ] self._tflist.append( tf.TransFuncAtRedshift(new_spec_arr, self.dlnz * dlnz_factor)) else: raise TypeError('invalid coarsen_type.') self._rs = np.array([tfunc.rs[0] for tfunc in new_tflist]) self._dlnz *= dlnz_factor
def rebin_N_arr(N_arr, in_eng, out_eng=None, spec_type='dNdE', log_bin_width=None): """Rebins an array of particle number with fixed energy. Returns an array or a `Spectrum` object. The rebinning conserves both total number and total energy. Parameters ---------- N_arr : ndarray An array of number of particles in each bin. in_eng : ndarray An array of the energy abscissa for each bin. The total energy in each bin `i` should be `N_arr[i]*in_eng[i]`. out_eng : ndarray, optional The new abscissa to bin into. If unspecified, assumed to be in_eng. spec_type : {'N', 'dNdE'}, optional The spectrum type to be output. Default is 'dNdE'. log_bin_width : ndarray, optional The bin width of the output abscissa. Returns ------- Spectrum The output `Spectrum` with appropriate dN/dE, with abscissa out_eng. Raises ------ OverflowError The maximum energy in `out_eng` cannot be smaller than any bin in `self.eng`. Notes ----- The total number and total energy is conserved by assigning the number of particles N in a bin of energy eng to two adjacent bins in new_eng, with energies eng_low and eng_upp such that eng_low < eng < eng_upp. Then dN_low_dE_low = (eng_upp - eng)/(eng_upp - eng_low)*(N/(E * dlogE_low)), and dN_upp_dE_upp = (eng - eng_low)/(eng_upp - eng_low)*(N/(E*dlogE_upp)). If a bin in `in_eng` is below the lowest bin in `out_eng`, then the total number and energy not assigned to the lowest bin are assigned to the underflow. Particles will only be assigned to the lowest bin if there is some overlap between the bin index with respect to `out_eng` bin centers is larger than -1.0. If a bin in `in_eng` is above the highest bin in `out_eng`, then an `OverflowError` is thrown. See Also -------- spectrum.Spectrum.rebin """ from darkhistory.spec.spectrum import Spectrum # This avoids circular dependencies. if N_arr.size != in_eng.size: raise TypeError( "The array for number of particles has a different length from the abscissa." ) if out_eng is None: if log_bin_width is None: log_bin_width = get_log_bin_width(in_eng) return Spectrum(in_eng, N_arr / (in_eng * log_bin_width)) if not np.all(np.diff(out_eng) > 0): raise TypeError("new abscissa must be ordered in increasing energy.") if out_eng[-1] < in_eng[-1]: raise OverflowError( "the new abscissa lies below the old one: this function cannot handle overflow (yet?)." ) # Get the bin indices that the current abscissa (self.eng) corresponds to in the new abscissa (new_eng). Can be any number between 0 and self.length-1. Bin indices are wrt the bin centers. # Add an additional bin at the lower end of out_eng so that underflow can be treated easily. first_bin_eng = np.exp( np.log(out_eng[0]) - (np.log(out_eng[1]) - np.log(out_eng[0]))) new_eng = np.insert(out_eng, 0, first_bin_eng) # Find the relative bin indices for in_eng wrt new_eng. The first bin in new_eng has bin index -1. bin_ind_interp = interp1d(new_eng, np.arange(new_eng.size) - 1, bounds_error=False, fill_value=(-2, new_eng.size)) bin_ind = bin_ind_interp(in_eng) # Locate where bin_ind is below 0, above self.length-1 and in between. ind_low = np.where(bin_ind < 0) ind_high = np.where(bin_ind == new_eng.size) ind_reg = np.where((bin_ind >= 0) & (bin_ind <= new_eng.size - 1)) # if ind_high[0].size > 0: # raise OverflowError("the new abscissa lies below the old one: this function cannot handle overflow (yet?).") # Get the total N and toteng in each bin toteng_arr = N_arr * in_eng N_arr_low = N_arr[ind_low] N_arr_high = N_arr[ind_high] N_arr_reg = N_arr[ind_reg] toteng_arr_low = toteng_arr[ind_low] # Bin width of the new array. Use only the log bin width, so that dN/dE = N/(E d log E) if log_bin_width is None: new_E_dlogE = new_eng * np.diff(np.log(get_bin_bound(new_eng))) else: new_log_bin_width = np.insert(log_bin_width, 0, log_bin_width[0]) new_E_dlogE = new_eng * new_log_bin_width # Regular bins first, done in a completely vectorized fashion. # reg_bin_low is the array of the lower bins to be allocated the particles in N_arr_reg, similarly reg_bin_upp. This should also take care of the fact that bin_ind is an integer. reg_bin_low = np.floor(bin_ind[ind_reg]).astype(int) reg_bin_upp = reg_bin_low + 1 # Takes care of the case where in_eng[-1] = out_eng[-1] reg_bin_low[reg_bin_low == new_eng.size - 2] = new_eng.size - 3 reg_bin_upp[reg_bin_upp == new_eng.size - 1] = new_eng.size - 2 reg_N_low = (reg_bin_upp - bin_ind[ind_reg]) * N_arr_reg reg_N_upp = (bin_ind[ind_reg] - reg_bin_low) * N_arr_reg reg_dNdE_low = ((reg_bin_upp - bin_ind[ind_reg]) * N_arr_reg / new_E_dlogE[reg_bin_low + 1]) reg_dNdE_upp = ((bin_ind[ind_reg] - reg_bin_low) * N_arr_reg / new_E_dlogE[reg_bin_upp + 1]) # Low bins. low_bin_low = np.floor(bin_ind[ind_low]).astype(int) N_above_underflow = np.sum((bin_ind[ind_low] - low_bin_low) * N_arr_low) eng_above_underflow = N_above_underflow * new_eng[1] N_underflow = np.sum(N_arr_low) - N_above_underflow eng_underflow = np.sum(toteng_arr_low) - eng_above_underflow low_dNdE = N_above_underflow / new_E_dlogE[1] new_dNdE = np.zeros(new_eng.size) new_dNdE[1] += low_dNdE # reg_dNdE_low = -1 refers to new_eng[0] np.add.at(new_dNdE, reg_bin_low + 1, reg_dNdE_low) np.add.at(new_dNdE, reg_bin_upp + 1, reg_dNdE_upp) # new_dNdE[reg_bin_low+1] += reg_dNdE_low # new_dNdE[reg_bin_upp+1] += reg_dNdE_upp # Generate the new Spectrum. out_spec = Spectrum(new_eng[1:], new_dNdE[1:]) if spec_type == 'N': out_spec.switch_spec_type() elif spec_type != 'dNdE': raise TypeError('invalid spec_type.') out_spec.underflow['N'] += N_underflow out_spec.underflow['eng'] += eng_underflow return out_spec
def get_pppc_spec(mDM, eng, pri, sec, decay=False): """ Returns the PPPC4DMID spectrum. This is the secondary spectrum to e+e-/photons normalized to one annihilation or decay event to the species specified in ``pri``. These results include electroweak corrections. The full list of allowed channels is: - :math:`\\delta`\ -function injections: ``elec_delta, phot_delta`` - Leptons: ``e_L, e_R, e, mu_L, mu_R, mu, tau_L, tau_R, tau`` - Quarks: ``q, c, b, t`` - Gauge bosons: ``gamma, g, W_L, W_T, W, Z_L, Z_T, Z`` - Higgs: ``h`` ``elec_delta`` and ``phot_delta`` assumes annihilation or decay to two electrons and photons respectively with no EW corrections or ISR/FSR. Variables with subscripts, e.g. ``e_L``, correspond to particles with different polarizations. These polarizations are suitably averaged to obtain the spectra returned in their corresponding variables without subscripts, e.g. ``e``. Parameters ---------- mDM : float The mass of the annihilating/decaying dark matter particle (in eV). eng : ndarray The energy abscissa for the output spectrum (in eV). pri : string One of the available channels (see above). sec : {'elec', 'phot'} The secondary spectrum to obtain. decay : bool, optional If ``True``, returns the result for decays. Returns ------- Spectrum Output :class:`.Spectrum` object, ``spec_type == 'dNdE'``. """ if decay: # Primary energies is for 1 GeV decay = 0.5 GeV annihilation. _mDM = mDM / 2. else: _mDM = mDM if _mDM < mass_threshold[pri]: # This avoids the absurd situation where mDM is less than the # threshold but we get a nonzero spectrum due to interpolation. raise ValueError( 'mDM is below the threshold to produce pri particles.') if pri == 'elec_delta': # Exact kinetic energy of each electron. if not decay: eng_elec = mDM - phys.me else: eng_elec = (mDM - 2 * phys.me) / 2 # Find the correct bin in eleceng. eng_to_inj = eng[eng < eng_elec][-1] # Place 2*eng_elec worth of electrons into that bin. Use # rebinning to accomplish this. if sec == 'elec': return rebin_N_arr(np.array([2 * eng_elec / eng_to_inj]), np.array([eng_to_inj]), eng) elif sec == 'phot': return Spectrum(eng, np.zeros_like(eng), spec_type='dNdE') else: raise ValueError('invalid sec.') if pri == 'phot_delta': # Exact kinetic energy of each photon. if not decay: eng_phot = mDM else: eng_phot = mDM / 2 # Find the correct bin in photeng. eng_to_inj = eng[eng < eng_phot][-1] # Place 2*eng_phot worth of photons into that bin. Use # rebinning to accomplish this. if sec == 'elec': return Spectrum(eng, np.zeros_like(eng), spec_type='dNdE') elif sec == 'phot': return rebin_N_arr(np.array([2 * eng_phot / eng_to_inj]), np.array([eng_to_inj]), eng) else: raise ValueError('invalid sec.') log10x = np.log10(eng / _mDM) # Refine the binning so that the spectrum is accurate. # Do this by checking that in the relevant range, there are at # least 50,000 bins. If not, double (unless an absurd number # of bins already). if (log10x[(log10x < 1) & (log10x > 1e-9)].size > 0 and log10x.size < 500000): while log10x[(log10x < 1) & (log10x > 1e-9)].size < 50000: log10x = np.interp(np.arange(0, log10x.size - 0.5, 0.5), np.arange(log10x.size), log10x) # Get the interpolator. dlNdlxIEW_interp = load_data('pppc') # Get the spectrum from the interpolator. dN_dlog10x = 10**dlNdlxIEW_interp[sec][pri].get_val(_mDM / 1e9, log10x) # Recall that dN/dE = dN/dlog10x * dlog10x/dE x = 10**log10x spec = Spectrum(x * _mDM, dN_dlog10x / (x * _mDM * np.log(10)), spec_type='dNdE') # Rebin down to the original binning. # The highest bin of spec.eng should be the same as eng[-1], based on # the interpolation strategy above. However, sometimes a floating point # error is picked up. We'll get rid of this so that rebin doesn't # complain. spec.eng[-1] = eng[-1] spec.rebin(eng) return spec