Esempio n. 1
0
    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.")
Esempio n. 2
0
    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.')
Esempio n. 3
0
    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
Esempio n. 4
0
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)
Esempio n. 5
0
 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)
     ])
Esempio n. 6
0
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
Esempio n. 7
0
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
    )
Esempio n. 8
0
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 
Esempio n. 9
0
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
Esempio n. 10
0
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.')
Esempio n. 11
0
    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
Esempio n. 12
0
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
Esempio n. 13
0
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