Exemple #1
0
def get_kappa_2s(photspec):
    """ Compute kappa_2s for use in kappa_DM function

    Parameters
    ----------
    photspec : Spectrum object
        spectrum of photons. spec.toteng() should return Energy per baryon.

    Returns
    -------
    kappa_2s : float
        The added photoionization rate from the 1s to the 2s state due to DM photons.
    """
    # Convenient Variables
    eng = photspec.eng
    rs = photspec.rs
    Lambda = phys.width_2s1s_H
    Tcmb = phys.TCMB(rs)
    lya_eng = phys.lya_eng

    # Photon phase space density (E >> kB*T approximation)
    def Boltz(E):
        return np.exp(-E / Tcmb)

    bounds = spectools.get_bin_bound(eng)
    mid = spectools.get_indx(bounds, lya_eng / 2)

    # Phase Space Density of DM
    f_nu = photspec.dNdE * phys.c**3 / (8 * np.pi * (eng / phys.hbar)**2)

    # Complementary (E - h\nu) phase space density of DM
    f_nu_p = np.zeros(mid)

    # Index of point complementary to eng[k]
    comp_indx = spectools.get_indx(bounds, lya_eng - eng[0])

    # Find the bin in which lya_eng - eng[k] resides. Store f_nu of that bin in f_nu_p.
    for k in np.arange(mid):
        while (lya_eng - eng[k]) < bounds[comp_indx]:
            comp_indx -= 1
        f_nu_p[k] = f_nu[comp_indx]

    # Setting up the numerical integration

    # Bin sizes
    diffs = np.append(bounds[1:mid], lya_eng / 2) - np.insert(
        bounds[1:mid], 0, 0)
    diffs /= (2 * np.pi * phys.hbar)

    dLam_dnu = phys.get_dLam2s_dnu()
    rates = dLam_dnu(eng[:mid] / (2 * np.pi * phys.hbar))

    boltz = Boltz(eng[:mid])
    boltz_p = Boltz(lya_eng - eng[:mid])

    # The Numerical Integral
    kappa_2s = np.sum(diffs * rates * (f_nu[:mid] + boltz) *
                      (f_nu_p + boltz_p)) / phys.width_2s1s_H - Boltz(lya_eng)

    return kappa_2s
Exemple #2
0
def ionize_s_cs_H_2(E_in, E_sec):
    '''
    Calculates the integrated, singly-differential ionization cross section (xsec) 
    for electrons impacting H at a particular 
    kinetic energy of the incident over a log-space vector of secondary energies. 
    
    Parameters
    ----------
    E_in : float
        Primary electron's initial kinetic energy (eV).
    E_sec : ndarray ([Spectrum].eng)
        The energy of one secondary electron for each initial electron (eV).

    Returns
    ----------
    sigma : ndarray
        The cross section for ionization integrated over a log-bin centered at each
        secondary energy (E_sec[n]); (given in cm^2). 
    
    See Also
    --------
    ionize_cs : Gives total ionization xsec
    ionize_s_cs: Gives individual values for multi-atom singly-diff xsecs
    '''

    #initialize return variable
    sigma = numpy.zeros(len(E_sec))

    #get bin boundaries for integration
    edges = spectools.get_bin_bound(E_sec)

    def integrand(W):  #W=E_sec[n]
        T = E_in
        B = 13.6057  #eV: binding energy
        U = 13.6057  #eV:
        t = T / B
        w = W / B
        y = 1 / (w + 1)
        df_dw = -0.022473 * y**2 + 1.1775 * y**3 - 0.46264 * y**4 + 0.089064 * y**5
        N = 1  # number of bound electrons in subshell
        N_i = 0.4343  #integral of df/dw from 0 to infinity
        u = U / B
        S = 4 * math.pi * p.value('Bohr radius')**2 * N * (13.6057 /
                                                           B)**2  #m^2

        return S / (B * (t + (u + 1))) * (
            (N_i / N - 2) / (t + 1) * (1 / (w + 1) + 1 / (t - w)) *
            (2 - N_i / N) * (1 / (w + 1)**2 + 1 / (t - w)**2) + numpy.log(t) /
            (N * (w + 1)) * df_dw)  #cm^2/eV

    #generates vector for summation
    integrand_vec = np.zeros(len(E_sec))

    #change begins
    B = 13.6057
    f_max = (E_in - B) / 2
    for i, E_s in enumerate(E_sec):
        if E_s > E_in - B:
            integrand_vec[i] = 0
        elif E_s > f_max:
            f_delta = E_s - f_max
            integrand_vec[i] = integrand(E_s - 2 * f_delta)
        else:
            integrand_vec[i] = integrand(E_s)

    #change ends

    #performs summation
    for n in range(len(E_sec) - 1):
        sigma[n] = integrate.trapz(integrand_vec[n:n + 2], E_sec[n:n + 2])

    #integrates continuously
    #for n in range(len(E_sec)):
    #sigma[n] = integrate.quad(integrand, edges[n], edges[n+1])[0]

    sigma = sigma.clip(min=0)
    return sigma
def compute_dep_inj_ionization_ratio(photon_spectrum,
                                     n,
                                     tot_inj,
                                     method='old'):
    """ Given a spectrum of deposited photons, resolve its energy into continuum photons, HI excitation, and HI, HeI, HeII ionization in that order.  The
        spectrum must provide the energy density of photons per unit time within each bin, not just the total energy within each bin.
        Q: can photons heat the IGM?  Should this method keep track of the fact that x_e, xHII, etc. are changing?

    Parameters
    ----------
    photon_spectrum : Spectrum object
        spectrum of photons
    n : list of floats
        density of (HI, HeI, HeII).
    tot_inj : float
        total energy injected by DM
    method : {'old','ion','new'}
        'old': All photons >= 13.6eV ionize hydrogen, within [10.2, 13.6)eV excite hydrogen, < 10.2eV are labelled continuum.
        'ion': Same as 'old', but now photons >= 13.6 can ionize HeI and HeII also.
        'new': Same as 'ion', but now [10.2, 13.6)eV photons treated more carefully

    Returns
    -------
    tuple of floats
        Ratio of deposited energy to a given channel over energy deposited by DM.
        The order of the channels is HI excitation and HI, HeI, HeII ionization
    """
    continuum, excite_HI, f_HI, f_HeI, f_HeII = 0, 0, 0, 0, 0

    continuum = photon_spectrum.toteng(bound_type='eng',
                                       bound_arr=np.array([
                                           photon_spectrum.eng[0], phys.lya_eng
                                       ]))[0] / tot_inj
    ion_index = np.searchsorted(photon_spectrum.eng, phys.rydberg)

    if (method != 'new'):
        excite_HI = photon_spectrum.toteng(
            bound_type='eng', bound_arr=np.array([phys.lya_eng, phys.rydberg
                                                  ]))[0] / tot_inj
    if (method == 'old'):
        f_HI = photon_spectrum.toteng(bound_type='eng',
                                      bound_arr=np.array([
                                          phys.rydberg, photon_spectrum.eng[-1]
                                      ]))[0] / tot_inj
    elif (method == 'ion'):
        # probability of being absorbed within time step dt in channel a = \sigma(E)_a n_a c*dt
        # First convert from probability of being absorbed in channel 'a' to conditional probability given that these are deposited photons
        # TODO: could be improved to include the missing [13.6,ion_bin]
        ionHI, ionHeI, ionHeII = [
            phys.photo_ion_xsec(photon_spectrum.eng[ion_index:], channel) *
            n[i] for i, channel in enumerate(['H0', 'He0', 'He1'])
        ]
        totList = ionHI + ionHeI + ionHeII

        ion_bin = spectools.get_bin_bound(photon_spectrum.eng)[ion_index]
        print(ion_bin)
        print((sum(photon_spectrum.eng[ion_index:] *
                   photon_spectrum.N[ion_index:]) + photon_spectrum.toteng(
                       bound_type='eng',
                       bound_arr=np.array(
                           [photon_spectrum.eng[0], phys.rydberg]))[0] +
               photon_spectrum.toteng(
                   bound_type='eng',
                   bound_arr=np.array([phys.rydberg, ion_bin]))[0]) / tot_inj)
        temp = np.array(photon_spectrum.eng[ion_index:])
        np.insert(temp, 0, phys.rydberg)
        print((photon_spectrum.toteng(
            bound_type='eng',
            bound_arr=np.array([photon_spectrum.eng[0], phys.rydberg]))[0] +
               sum(photon_spectrum.toteng(bound_type='eng', bound_arr=temp))) /
              tot_inj)
        print(
            sum(
                photon_spectrum.toteng(bound_type='eng',
                                       bound_arr=photon_spectrum.eng)) /
            tot_inj)
        f_HI, f_HeI, f_HeII = [
            sum(photon_spectrum.eng[ion_index:] *
                photon_spectrum.N[ion_index:] * llist / totList) / tot_inj
            for llist in [ionHI, ionHeI, ionHeII]
        ]
        #f_HI, f_HeI, f_HeII = [sum(photon_spectrum.toteng(bound_type='eng',bound_arr=np.arange(ion_index-1,len(photon_spectrum.eng)))*llist/totList)/tot_inj for llist in [ionHI, ionHeI, ionHeII]]

        #There's an extra piece of energy between 13.6 amd the energy at ion_index
        #print(photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([phys.rydberg,photon_spectrum.eng[ion_index]]))[0]/tot_inj)
        #f_HI = f_HI + photon_spectrum.toteng(bound_type='eng', bound_arr=np.array([phys.rydberg,photon_spectrum.eng[ion_index]]))[0]/tot_inj
    return continuum, excite_HI, f_HI, f_HeI, f_HeII
Exemple #4
0
    def toteng(self, bound_type=None, bound_arr=None):
        """Returns the total energy of particles in part of the spectra.

        The part of the `Spectrum` objects to find the total energy of particles can be specified in two ways, and is specified by `bound_type`. Multiple totals can be obtained through `bound_arr`.

        Parameters
        ----------
        bound_type : {'bin', 'eng', None}
            The type of bounds to use. Bound values do not have to be within the [0:eng.size] for `'bin'` or within the abscissa for `'eng'`. `None` should only be used when computing the total particle number in the spectrum. 

            Specifying ``bound_type=='bin'`` without bound_arr gives the total energy in each bin. 

        bound_arr : ndarray of length N, optional
            An array of boundaries (bin or energy), between which the total number of particles will be computed. If unspecified, the total number of particles in the whole spectrum is computed.

            For 'bin', bounds are specified as the bin *boundary*, with 0 being the left most boundary, 1 the right-hand of the first bin and so on. This is equivalent to integrating over a histogram. For 'eng', bounds are specified by energy values.

            These boundaries need not be integer values for 'bin': specifying np.array([0.5, 1.5]) for example will include half of the first bin and half of the second.

        Returns
        -------
        ndarray of shape (self.rs.size, N-1) or length N-1
            Total number of particles in the spectra or between the specified boundaries.

        Examples
        --------
        >>> from darkhistory.spec.spectrum import Spectrum
        >>> eng = np.array([1, 10, 100, 1000])
        >>> spec_arr = [Spectrum(eng, np.arange(4) + 4*i, rs=100, spec_type='N') for i in np.arange(4)]
        >>> test_spectra = Spectra(spec_arr)
        >>> test_spectra.toteng()
        array([ 3210.,  7654.,  12098.,  16542.])
        >>> test_spectra.toteng('bin', np.array([1, 3]))
        array([[ 210., 650., 1090., 1530.]])
        >>> test_spectra.toteng('eng', np.array([10, 1e4]))
        array([[ 3205., 7625., 12045., 16465.]])

        See Also
        ---------
        :meth:`Spectra.totN`

        """
        log_bin_width = get_log_bin_width(self.eng)

        # Using the broadcasting rules here.
        if self.spec_type == 'dNdE':
            dNdlogE = self.grid_vals * self.eng
        elif self.spec_type == 'N':
            dNdlogE = self.grid_vals / log_bin_width

        if bound_type is not None:

            if bound_arr is None:

                return dNdlogE * self.eng * log_bin_width

            if bound_type == 'bin':

                if not all(np.diff(bound_arr) >= 0):
                    raise TypeError('bound_arr must have increasing entries.')

                # Size is number of totals requested x number of Spectrums.
                eng_in_bin = np.zeros((bound_arr.size - 1, self.in_eng.size))

                if bound_arr[0] > self.eng.size or bound_arr[-1] < 0:
                    return eng_in_bin

                for i, (low,
                        upp) in enumerate(zip(bound_arr[:-1], bound_arr[1:])):

                    # Set the lower and upper bounds, including case where
                    # low and upp are outside of the bins.
                    if low > self.eng.size or upp < 0:
                        continue

                    low_ceil = int(np.ceil(low))
                    low_floor = int(np.floor(low))
                    upp_ceil = int(np.ceil(upp))
                    upp_floor = int(np.floor(upp))

                    # Sum the bins that are completely between the bounds.

                    eng_full_bins = np.dot(
                        dNdlogE[:, low_ceil:upp_floor] *
                        self.eng[low_ceil:upp_floor],
                        log_bin_width[low_ceil:upp_floor])

                    eng_part_bins = np.zeros_like(self.in_eng)

                    if low_floor == upp_floor or low_ceil == upp_ceil:

                        # Bin indices are within the same bin. The second
                        # requirement covers the case where upp_ceil is
                        # eng.size.

                        eng_part_bins += (dNdlogE[:, low_floor] * (upp - low) *
                                          self.eng[low_floor] *
                                          log_bin_width[low_floor])

                    else:
                        # Add up part of the bin for the low partial bin
                        # and the high partial bin.
                        eng_part_bins += (dNdlogE[:, low_floor] *
                                          (low_ceil - low) *
                                          self.eng[low_floor] *
                                          log_bin_width[low_floor])

                        if upp_floor < self.eng.size:
                            # If upp_floor is eng.size, then there is no
                            # partial bin for the upper index.
                            eng_part_bins += (dNdlogE[:, upp_floor] *
                                              (upp - upp_floor) *
                                              self.eng[upp_floor] *
                                              log_bin_width[upp_floor])

                    eng_in_bin[i] = eng_full_bins + eng_part_bins

                return eng_in_bin

            if bound_type == 'eng':
                bin_boundary = get_bin_bound(self.eng)
                eng_bin_ind = np.interp(np.log(bound_arr),
                                        np.log(bin_boundary),
                                        np.arange(bin_boundary.size),
                                        left=0,
                                        right=self.eng.size + 1)

                return self.toteng('bin', eng_bin_ind)

        else:
            return (np.dot(dNdlogE, self.eng * log_bin_width) +
                    self.eng_underflow)
Exemple #5
0
    def toteng(self, bound_type=None, bound_arr=None):
        """Returns the total energy of particles in part of the spectrum.

        The part of the spectrum can be specified in two ways, and is specified by bound_type. Multiple totals can be obtained through bound_arr.

        Parameters
        ----------
        bound_type : {'bin', 'eng', None}
            The type of bounds to use. Bound values do not have to be within the [0:length] for 'bin' or within the abscissa for 'eng'. None should only be used to obtain the total energy.

            Specifying ``bound_type='bin'`` without bound_arr gives the total energy in each bin.

        bound_arr : ndarray of length N, optional
            An array of boundaries (bin or energy), between which the total number of particles will be computed. If unspecified, the total number of particles in the whole spectrum is computed.

            For 'bin', bounds are specified as the bin *boundary*, with 0 being the left most boundary, 1 the right-hand of the first bin and so on. This is equivalent to integrating over a histogram. For 'eng', bounds are specified by energy values.

            These boundaries need not be integer values for 'bin': specifying np.array([0.5, 1.5]) for example will include half of the first bin and half of the second.


        Returns
        -------
        ndarray of length N-1, or float
            Total energy in the spectrum or between the specified boundaries.

        Examples
        ---------
        >>> eng = np.array([1, 10, 100, 1000])
        >>> N   = np.array([1, 2, 3, 4])
        >>> spec = Spectrum(eng, N, spec_type='N')
        >>> spec.toteng()
        4321.0
        >>> spec.toteng('bin', np.array([1, 3]))
        array([320.])
        >>> spec.toteng('eng', np.array([10, 1e4]))
        array([4310.])

        See Also
        ---------
        :meth:`.Spectrum.totN`
        
        """
        eng = self.eng
        length = self.length
        log_bin_width = get_log_bin_width(self.eng)

        if self._spec_type == 'dNdE':
            dNdlogE = self.eng * self.dNdE
        elif self._spec_type == 'N':
            dNdlogE = self.N / log_bin_width

        if bound_type is not None:

            if bound_arr is None:

                return dNdlogE * eng * log_bin_width

            if bound_type == 'bin':

                if not all(np.diff(bound_arr) >= 0):

                    raise TypeError("bound_arr must have increasing entries.")

                eng_in_bin = np.zeros(bound_arr.size - 1)

                if bound_arr[0] > length or bound_arr[-1] < 0:
                    return eng_in_bin

                for low, upp, i in zip(bound_arr[:-1], bound_arr[1:],
                                       np.arange(eng_in_bin.size)):

                    if low > length or upp < 0:
                        eng_in_bin[i] = 0
                        continue

                    low_ceil = int(np.ceil(low))
                    low_floor = int(np.floor(low))
                    upp_ceil = int(np.ceil(upp))
                    upp_floor = int(np.floor(upp))
                    # Sum the bins that are completely between the bounds.
                    eng_full_bins = np.dot(
                        eng[low_ceil:upp_floor] * dNdlogE[low_ceil:upp_floor],
                        log_bin_width[low_ceil:upp_floor])

                    eng_part_bins = 0

                    if low_floor == upp_floor or low_ceil == upp_ceil:
                        # Bin indices are within the same bin. The second requirement covers the case where upp_ceil is length.
                        eng_part_bins += (eng[low_floor] * dNdlogE[low_floor] *
                                          (upp - low) *
                                          log_bin_width[low_floor])
                    else:
                        # Add up part of the bin for the low partial bin and the high partial bin.
                        eng_part_bins += (eng[low_floor] * dNdlogE[low_floor] *
                                          (low_ceil - low) *
                                          log_bin_width[low_floor])
                        if upp_floor < length:
                            # If upp_floor is length, then there is no partial bin for the upper index.
                            eng_part_bins += (eng[upp_floor] *
                                              dNdlogE[upp_floor] *
                                              (upp - upp_floor) *
                                              log_bin_width[upp_floor])

                    eng_in_bin[i] = eng_full_bins + eng_part_bins

                return eng_in_bin

            if bound_type == 'eng':
                bin_boundary = get_bin_bound(self.eng)
                eng_bin_ind = np.interp(np.log(bound_arr),
                                        np.log(bin_boundary),
                                        np.arange(bin_boundary.size),
                                        left=0,
                                        right=length + 1)

                return self.toteng('bin', eng_bin_ind)

        else:
            return (np.dot(dNdlogE, eng * log_bin_width) +
                    self.underflow['eng'])