Example #1
0
def compton_cooling_rate(xHII, xHeII, xHeIII, T_m, rs):
    """Returns the Compton cooling rate.

    Parameters
    ----------
    xHII : float
        n_HII/n_H.
    xHeII : float
        n_HeII/n_H.
    xHeIII : float
        n_HeIII/n_H.
    T_m : float
        The matter temperature.
    rs : float
        The redshift in 1+z.

    Returns
    -------
    float
        The Compton cooling rate in eV/s.

    Notes
    -----
    This is the energy loss rate, *not* the temperature loss rate.

    """
    xe = xHII + xHeII + 2 * xHeIII

    return (4 * phys.thomson_xsec * 4 * phys.stefboltz / phys.me * xe *
            phys.nH * rs**3 * (phys.TCMB(rs) - T_m) * phys.TCMB(rs)**4)
Example #2
0
        def dyHeII_dz(yHII, yHeII, yHeIII, log_T_m, rs):

            T_m = np.exp(log_T_m)

            if not helium_TLA:

                return 0

            if chi - xHeII(yHeII) < 1e-6 and rs < 100:
                # At this point, leave at 1 - 1e-6
                return 0

            # Stop the solver from reaching these extremes.
            if yHeII > 14 or yHeII < -14:
                return 0

            # # Use the Saha values at high ionization.
            # if xHeII(yHeII) > 0.995*chi:

            #     # print(phys.d_xe_Saha_dz(rs, 'HeI'))

            #     return (
            #         2/chi * np.cosh(yHeII)**2 * phys.d_xe_Saha_dz(rs, 'HeI')
            #     )

            xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII)
            ne = xe * nH
            xHI = 1 - xHII(yHII)
            xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII)

            term_recomb_singlet = (xHeII(yHeII) * xe * nH *
                                   phys.alpha_recomb(T_m, 'HeI_21s'))
            term_ion_singlet = (
                phys.beta_ion(phys.TCMB(rs), 'HeI_21s') *
                (chi - xHeII(yHeII)) *
                np.exp(-phys.He_exc_eng['21s'] / phys.TCMB(rs)))

            term_recomb_triplet = (xHeII(yHeII) * xe * nH *
                                   phys.alpha_recomb(T_m, 'HeI_23s'))
            term_ion_triplet = (
                3 * phys.beta_ion(phys.TCMB(rs), 'HeI_23s') *
                (chi - xHeII(yHeII)) *
                np.exp(-phys.He_exc_eng['23s'] / phys.TCMB(rs)))

            return 2 / chi * np.cosh(yHeII)**2 * phys.dtdz(rs) * (
                -phys.C_He(xHII(yHII), xHeII(yHeII), rs, 'singlet') *
                (term_recomb_singlet - term_ion_singlet) -
                phys.C_He(xHII(yHII), xHeII(yHeII), rs, 'triplet') *
                (term_recomb_triplet - term_ion_triplet) +
                _f_He_ion(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate /
                (phys.He_ion_eng * nH))
Example #3
0
def d_x_be_Saha_dz(rs, alphaD, m_be, m_bp, xi):
    """`z`-derivative of the Saha equilibrium ionization value.

    Parameters
    ----------
    rs : float
        The redshift in 1+z.
    species : {'singlet', 'triplet'}

    Returns
    -------
    float
        The derivative of the Saha equilibrium d xe/dz.

    Notes
    -----
    See astro-ph/9909275 and 1011.3758 for details.
    """

    x_be = x_be_Saha(rs, alphaD, m_be, m_bp, xi)

    T = xi * phys.TCMB(rs)

    B_D = get_BD(alphaD, m_be, m_bp)

    numer = (B_D / T - 3 / 2) * x_be**2 * (1 - x_be)
    denom = rs * (2 * x_be * (1 - x_be) + x_be**2)

    #return phys.d_xe_Saha_dz(rs, 'HI')
    return numer / denom
Example #4
0
def norm_compton_cooling_rate(x_be, T_DM, rs, alphaD, m_be, m_bp, xi):
    """ Gamma_c where dlogT_DM/dt = Gamma_c (T_D - T_DM), in 1/s
    """
    T_D = xi * phys.TCMB(rs)
    pre = 64 * np.pi**3 * alphaD**2 / 135 * T_D**4 * x_be / (1 + x_be)
    mass = (1 + (m_be / m_bp)**3) / m_be**3
    return pre * mass / phys.hbar
Example #5
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
Example #6
0
def Gam_pi(x_be, T_DM, rs, alphaD, m_be, m_bp, xi):
    B_D = get_BD(alphaD, m_be, m_bp)
    T_D = xi * phys.TCMB(rs)
    m_D = m_be + m_bp - B_D
    mu_D = m_be * m_bp / m_D
    n_D = phys.rho_DM / m_D * rs**3
    denom = (3 / 2 * T_DM * n_D * (1 + x_be))

    # Calculate x_2s using Hyrec's steady stateassumption #
    Lya_D = 3 / 4 * B_D
    dark_sobolev = 2**7 * alphaD**3 * B_D * n_D * phys.c**3 * (1 - x_be) / (
        3**7 * 2 * np.pi * phys.hbar * phys.hubble(rs) * Lya_D)
    R_D = 2**9 * alphaD**3 * B_D / 3**8 * (
        1 - np.exp(-dark_sobolev)) / dark_sobolev

    Lam = (alphaD / phys.alpha)**6 * (B_D / phys.rydberg) * phys.width_2s1s_H
    alpha_B = dark_alpha_recomb(T_DM, alphaD, m_be, m_bp, xi)
    beta_e = dark_beta_ion(T_D, alphaD, m_be, m_bp, xi)
    x_2 = (n_D * x_be**2 * alpha_B + (3 * R_D + Lam) *
           (1 - x_be) * np.exp(-B_D / T_D)) / (beta_e + 3 / 4 * R_D +
                                               1 / 4 * Lam)
    x_2s = 1 / 4 * x_2

    pre = alphaD**3 * T_D**2 / (3 * np.pi) * np.exp(-B_D / (4 * T_D))
    convert = phys.hbar
    return pre * x_2s * n_D * F_pi(T_D / B_D) / denom * convert
Example #7
0
        def dyHII_dz(yHII, yHeII, yHeIII, log_T_m, rs):

            T_m = np.exp(log_T_m)

            if 1 - xHII(yHII) < 1e-6 and rs < 100:
                # At this point, leave at 1 - 1e-6
                return 0
            # if yHII > 14. or yHII < -14.:
            #     # Stops the solver from wandering too far.
            #     return 0
            if xHeII(yHeII) > 0.99 * chi and rs > 1500:
                # This is prior to helium recombination.
                # Assume H completely ionized.
                return 0

            if helium_TLA and xHII(yHII) > 0.999 and rs > 1500:
                # Use the Saha value.
                return 2 * np.cosh(yHII)**2 * phys.d_xe_Saha_dz(rs, 'HI')

            if not helium_TLA and xHII(yHII) > 0.99 and rs > 1500:
                # Use the Saha value.
                return 2 * np.cosh(yHII)**2 * phys.d_xe_Saha_dz(rs, 'HI')

            xe = xHII(yHII) + xHeII(yHeII) + 2 * xHeIII(yHeIII)
            ne = xe * nH
            xHI = 1 - xHII(yHII)
            xHeI = chi - xHeII(yHeII) - xHeIII(yHeIII)

            return 2 * np.cosh(yHII)**2 * phys.dtdz(rs) * (
                # Recombination processes.
                # Boltzmann factor is T_r, agrees with HyREC paper.
                -phys.peebles_C(xHII(yHII), rs) *
                (phys.alpha_recomb(T_m, 'HI') * xHII(yHII) * xe * nH -
                 4 * phys.beta_ion(phys.TCMB(rs), 'HI') * xHI *
                 np.exp(-phys.lya_eng / phys.TCMB(rs)))
                # DM injection. Note that C = 1 at late times.
                + _f_H_ion(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate /
                (phys.rydberg * nH) + (1 - phys.peebles_C(xHII(yHII), rs)) *
                (_f_H_exc(rs, xHI, xHeI, xHeII(yHeII)) * inj_rate /
                 (phys.lya_eng * nH)))
Example #8
0
def Gam_pr(x_be, T_DM, rs, alphaD, m_be, m_bp, xi):
    B_D = get_BD(alphaD, m_be, m_bp)
    T_D = xi * phys.TCMB(rs)
    m_D = m_be + m_bp - B_D
    mu_D = m_be * m_bp / m_D
    n_D = phys.rho_DM / m_D * rs**3
    denom = (3 / 2 * T_DM * n_D * (1 + x_be))

    pre = 2 * alphaD**3 * np.sqrt(2 * np.pi * T_DM) / (3 * mu_D**(3 / 2))
    convert = (phys.hbar * phys.c)**3 / phys.hbar

    return pre * x_be**2 * n_D**2 * F_pr(T_D / B_D,
                                         T_DM / T_D) / denom * convert
Example #9
0
def Gam_R(x_be, T_DM, rs, alphaD, m_be, m_bp, xi):
    """ Rayleigh energy exchange rate / T_DM in 1/s """
    B_D = get_BD(alphaD, m_be, m_bp)
    T_D = xi * phys.TCMB(rs)
    m_D = m_be + m_bp - B_D
    n_D = phys.rho_DM / m_D * rs**3
    zeta_9 = 1.00201
    ratio = T_D / T_DM
    denom = (3 / 2 * (1 + x_be))

    pre = 430080 * zeta_9 * alphaD**2 * (1 - x_be) / np.pi**2
    temps = (T_D / B_D)**4 * (T_D / m_be)**2 * T_D / m_D * T_D / phys.hbar
    return pre * temps / denom  #ratio * convert
Example #10
0
def Gam_ff(x_be, T_DM, rs, alphaD, m_be, m_bp, xi):
    """ free-free absorption - emission (brem) / T_DM in 1/s """
    B_D = get_BD(alphaD, m_be, m_bp)
    T_D = xi * phys.TCMB(rs)
    eps = 1 - T_DM / T_D

    m_D = m_be + m_bp - B_D
    mu_D = m_be * m_bp / m_D
    n_D = phys.rho_DM / m_D * rs**3
    g_ff = 1.33
    zeta_3 = 1.20206
    denom = (3 / 2 * T_DM * n_D * (1 + x_be))

    pre = 16 * alphaD**3 * g_ff * x_be**2 * n_D**2 / (3 * mu_D)**(3 / 2)
    prnth = np.pi**2 * (1 + 2 * eps) / 6 - zeta_3 * eps
    convert = (phys.hbar * phys.c)**3 / phys.hbar
    return pre * np.sqrt(2 * np.pi * T_DM) * prnth / denom * convert
Example #11
0
def x_be_Saha(rs, alphaD, m_be, m_bp, xi):
    """Saha equilibrium ionization value for H and He.

    Parameters
    ----------
    rs : float
        The redshift in 1+z.
    species : {'HI', 'HeI'}
        The relevant species.

    Returns
    -------
    float
        The Saha equilibrium xe.

    Notes
    -----
    See astro-ph/9909275 and 1011.3758 for details.
    """
    T = xi * phys.TCMB(rs)

    B_D = get_BD(alphaD, m_be, m_bp)
    m_D = m_be + m_bp - B_D
    n_D = phys.rho_DM / m_D * rs**3
    #n_D = phys.nH * rs**3
    mu_D = m_be * m_bp / m_D

    de_broglie_wavelength = phys.c * 2 * np.pi * phys.hbar / np.sqrt(
        2 * np.pi * mu_D * T)

    rhs = (1 / de_broglie_wavelength)**3 / n_D * np.exp(-B_D / T)
    a = 1.
    b = rhs
    q = -rhs

    if rhs < 1e8:

        x_be = (-b + np.sqrt(b**2 - 4 * a * q)) / (2 * a)

    else:

        x_be = 1. - a / rhs

    return x_be
Example #12
0
def dark_peebles_C(x_be, rs, alphaD, m_be, m_bp, xi):
    B_D = get_BD(alphaD, m_be, m_bp)
    m_D = m_be + m_bp - B_D
    n_D = phys.rho_DM / m_D * rs**3
    #n_D = phys.nH*rs**3
    T_D = xi * phys.TCMB(rs)

    beta = dark_beta_ion(T_D, alphaD, m_be, m_bp, xi)

    Lya_D = 3 / 4 * B_D

    dark_sobolev = 2**7 * alphaD**3 * B_D * n_D * phys.c**3 * (1 - x_be) / (
        3**7 * 2 * np.pi * phys.hbar * phys.hubble(rs) * Lya_D)

    R_D_fac = 3 / 4 * 2**9 * alphaD**3 * B_D / 3**8 * (
        1 - np.exp(-dark_sobolev)) / dark_sobolev

    Lam_2s_1s_fac = 1 / 4 * (alphaD / phys.alpha)**6 * (
        B_D / phys.rydberg) * phys.width_2s1s_H

    ssum = R_D_fac + Lam_2s_1s_fac

    return ssum / (ssum + beta)
Example #13
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
    )
Example #14
0
def ics_spec(
    eleckineng, photeng, T, as_pairs=False, inf_upp_bound=True,
    thomson_tf=None, rel_tf=None, T_ref=None
):
    """ ICS spectrum of secondary photons.

    Switches between `thomson_spec` and `rel_spec`. 

    Parameters
    ----------
    eleckineng : ndarray
        Incoming electron energy. 
    photeng : ndarray
        Outgoing photon energy. 
    T : float
        CMB temperature. 
    as_pairs : bool, optional
        If True, treats eleckineng and photeng as a paired list: produces eleckineng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleckineng, returning an array of length eleckineng.size*photeng.size. 
    inf_upp_bound : bool
        If True, calculates the approximate relativistic spectrum that is used for fast interpolation over different values of T. See Notes for more details. Default is True.  
    thomson_tf : TransFuncAtRedshift, optional
        Reference Thomson ICS transfer function. If specified, calculation is done by interpolating over the transfer function. 
    rel_tf : TransFuncAtRedshift, optional
        Reference relativistic ICS transfer function. If specified, calculation is done by interpolating over the transfer function. 
    T_ref : float, optional
        The reference temperature at which the reference transfer functions is evaluated. If not specified, defaults to phys.TCMB(400).

    Returns
    -------
    TransFuncAtRedshift
        dN/(dt dE) of the outgoing photons, dt = 1 s, with `self.in_eng = eleckineng` and `self.eng = photeng`. `self.rs` is determined from `T`, and `self.dlnz` is normalized to 1 second. 

    Notes
    -----
    Insert note on the suitability of the method. 
    """

    if not inf_upp_bound and (thomson_tf is not None or rel_tf is not None):

        raise ValueError('inf_upp_bound must be True in order to use an interpolation over reference transfer functions.')

    gamma = eleckineng/phys.me + 1
    eleceng = eleckineng + phys.me

    if as_pairs:
        if eleceng.size != photeng.size:
            raise TypeError('Photon and electron energy arrays must have the same length for pairwise computation.')
        gamma_mask = gamma
        eleceng_mask = eleceng
        eleckineng_mask = eleckineng
        photeng_mask = photeng
        spec = np.zeros(gamma)
    else:
        gamma_mask = np.outer(gamma, np.ones(photeng.size))
        eleceng_mask = np.outer(eleceng, np.ones(photeng.size))
        eleckineng_mask = np.outer(eleckineng, np.ones(photeng.size))
        photeng_mask = np.outer(np.ones(eleceng.size), photeng)
        spec = np.zeros((eleceng.size, photeng.size), dtype='float128')

    rel_bound = 20

    rel = (gamma_mask > rel_bound)

    if T_ref is None:
        T_ref = phys.TCMB(400)

    y = T/T_ref

    if rel_tf != None:
        if as_pairs:
            raise TypeError('When reading from file, the keyword as_pairs is not supported.')
        # If the electron energy at which interpolation is to be taken is outside rel_tf, then an error should be returned, since the file has not gone up to high enough energies. 
        # Note relativistic spectrum is indexed by TOTAL electron energy.
        # rel_tf = rel_tf.at_in_eng(y*eleceng[gamma > rel_bound])
        # If the photon energy at which interpolation is to be taken is outside rel_tf, then for large photon energies, we set it to zero, since the spectrum should already be zero long before. If it is below, nan is returned, and the results should not be used.

        rel_tf_interp = np.transpose(
            rel_tf.interp_func(
                np.log(y*eleceng[gamma > rel_bound]), np.log(y*photeng)
            )
        )

        spec[rel] = y**4*rel_tf_interp.flatten()

    else: 
        spec[rel] = rel_spec(
            eleceng_mask[rel], photeng_mask[rel], T, 
            inf_upp_bound=inf_upp_bound, as_pairs=True
        )

    if thomson_tf != None:

        thomson_tf_interp = np.transpose(
            thomson_tf.interp_func(
                np.log(eleckineng[gamma <= rel_bound]), np.log(photeng/y)
            )
        )

        spec[~rel] = y**2*thomson_tf_interp.flatten()

    else:
        spec[~rel] = thomson_spec(
            eleckineng_mask[~rel], photeng_mask[~rel], 
            T, as_pairs=True
        )


    # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. Non-zero to take log interpolations later.
    spec[spec < 1e-100] = 1e-100

    if as_pairs:
        return spec
    else:

        rs = T/phys.TCMB(1)
        dlnz = -1./(phys.dtdz(rs)*rs)

        return TransFuncAtRedshift(
            spec, in_eng = eleckineng, eng = photeng, 
            rs = np.ones_like(eleckineng)*rs, dlnz=dlnz,
            spec_type = 'dNdE'
        )
Example #15
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 
Example #16
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
Example #17
0
def engloss_spec(
    eleckineng, delta, T, 
    as_pairs=False, thomson_only=False, thomson_tf=None, rel_tf=None,
):
    """ Thomson ICS scattered electron energy loss spectrum. 

    Switches between :func:`.engloss_spec_series` and :func:`.engloss_spec_diff` in the Thomson regime. Also switches between Thomson and relativistic regimes automatically.

    Parameters
    ----------
    eleckineng : ndarray
        Incoming electron kinetic energy. 
    delta : ndarray
        Energy gained by photon after upscattering (only positive values). 
    T : float
        CMB temperature.
    as_pairs : bool, optional
        If true, treats eleckineng and photeng as a paired list: produces eleckineng.size == photeng.size values. Otherwise, gets the spectrum at each photeng for each eleckineng, returning an array of length eleckineng.size*photeng.size. 
    thomson_only : bool, optional
        If true, only returns the Thomson energy loss spectrum, and never switches to the relativistic case. 
    thomson_tf : TransFuncAtRedshift, optional
        Reference Thomson energy loss ICS spectrum. If specified, calculation is done by interpolating over the transfer function. 
    rel_tf : TransFuncAtRedshift, optional
        Reference relativistic energy loss ICS spectrum. If specified, calculation is done by interpolating over the transfer function. 

    Returns
    -------
    TransFuncAtRedshift or ndarray
        dN/(dt d Delta) of the outgoing photons (dt = 1 s). If as_pairs == False, returns a TransFuncAtRedshift, with abscissa given by (eleckineng, delta). Otherwise, returns an ndarray, with abscissa given by each pair of (eleckineng, delta). 
    """

    gamma = eleckineng/phys.me + 1
    eleceng = eleckineng + phys.me
    beta = np.sqrt(eleckineng/phys.me*(gamma+1)/gamma**2)
    eta = delta/T

    # where to switch between Thomson and relativistic treatments.
    if thomson_only:
        rel_bound = np.inf 
    else:
        rel_bound = 20

    # 2D masks have dimensions (eleceng, delta).

    if as_pairs:
        if eleckineng.size != delta.size:
            raise TypeError('delta and electron energy arrays must have the same length for pairwise computation.')
        gamma_mask = gamma
        beta_mask = beta
        eleckineng_mask = eleckineng 
        eleceng_mask = eleceng
        delta_mask = delta
        spec = np.zeros_like(gamma)
    else:
        gamma_mask = np.outer(gamma, np.ones_like(eta))
        beta_mask = np.outer(beta, np.ones_like(eta))
        eleckineng_mask = np.outer(eleckineng, np.ones_like(eta))
        eleceng_mask = np.outer(eleceng, np.ones_like(eta))
        delta_mask = np.outer(np.ones_like(eleckineng), delta)
        spec = np.zeros(
            (eleckineng.size, delta.size), dtype='float128'
        )

    beta_small = beta_mask < 0.1
    
    rel = gamma_mask > rel_bound

    y = T/phys.TCMB(400)

    if not thomson_only:
        if rel_tf != None:
            if as_pairs:
                raise TypeError('When reading from file, the keyword as_pairs is not supported.')
            # If the electron energy at which interpolation is to be taken is outside rel_tf, then an error should be returned, since the file has not gone up to high enough energies.
            #rel_tf = rel_tf.at_in_eng(y*eleceng[gamma > rel_bound])
            # If the photon energy at which interpolation is to be taken is outside rel_tf, then for large photon energies, we set it to zero, since the spectrum should already be zero long before. If it is below, nan is returned, and the results should not be used.

            rel_tf_interp = np.transpose(
                rel_tf.interp_func(
                    np.log(y*eleceng[gamma > rel_bound]), np.log(y*delta)
                )
            )    

            spec[rel] = y**4*rel_tf_interp.flatten()

        else:

            print(
                '###### RELATIVISTIC ENERGY LOSS SPECTRUM ######'
            )

            spec[rel] = ics_spectrum.rel_spec(
                eleceng_mask[rel],
                delta_mask[rel],
                T, inf_upp_bound=True, as_pairs=True 
            )

            print('###### COMPLETE! ######')

    if thomson_tf != None:
        
        thomson_tf_interp = np.transpose(
            thomson_tf.interp_func(
                np.log(eleckineng[gamma <= rel_bound]), np.log(delta/y)
            )
        )

        spec[~rel] = y**2*thomson_tf_interp.flatten()

    else:
        print('###### THOMSON ENERGY LOSS SPECTRUM ######')
        # beta_small obviously doesn't intersect with rel. 
        spec[beta_small] = engloss_spec_diff(
            eleckineng_mask[beta_small], 
            delta_mask[beta_small], T, as_pairs=True
        )

        spec[~beta_small & ~rel] = engloss_spec_series(
            eleckineng_mask[~beta_small & ~rel],
            delta_mask[~beta_small & ~rel], T, as_pairs=True
        )
        print('###### COMPLETE! ######')

    # Zero out spec values that are too small (clearly no scatters within the age of the universe), and numerical errors. Non-zero to take log interpolation later. 
    spec[spec < 1e-100] = 1e-100
    
    if as_pairs:
        return spec 
    else:

        rs = T/phys.TCMB(1)
        dlnz = -1./(phys.dtdz(rs)*rs)

        return TransFuncAtRedshift(
            spec, in_eng = eleckineng, eng = delta, 
            rs = np.ones_like(eleckineng)*rs, dlnz=dlnz,
            spec_type = 'dNdE', with_interp_func=True
        )
Example #18
0
def get_ics_cooling_tf(
    raw_thomson_tf, raw_rel_tf, raw_engloss_tf,
    eleceng, photeng, rs, fast=True
):

    """Transfer function for complete electron cooling through ICS.

    Parameters
    ----------
    raw_thomson_tf : TransFuncAtRedshift
        Raw Thomson ICS scattered photon spectrum transfer function.
    raw_rel_tf : TransFuncAtRedshift
        Raw relativistic ICS scattered photon spectrum transfer function.
    raw_engloss_tf : TransFuncAtRedshift
        Raw Thomson ICS scattered electron net energy loss spectrum transfer function.
    eleceng : ndarray
        The electron *kinetic* energy abscissa.
    photeng : ndarray
        The photon energy abscissa.
    rs : float
        The redshift (1+z).
    fast : bool, optional
        If True, uses optimized code (with very little checks)

    Returns
    -------

    tuple of TransFuncAtRedshift
        Transfer functions for photons and low energy electrons.

    Notes
    -----
    The raw transfer functions should be generated when the code package is first installed. The transfer function corresponds to the fully resolved
    photon spectrum after scattering by one electron.

    """

    if fast:
        return get_ics_cooling_tf_fast(
            raw_thomson_tf, raw_rel_tf, raw_engloss_tf,
            eleceng, photeng, rs
        )


    T = phys.TCMB(rs)

    # Photon transfer function for single primary electron single scattering.
    # This is dN/(dE dt), dt = 1 s.
    ICS_tf = ics_spec(
        eleceng, photeng, T, thomson_tf = raw_thomson_tf, rel_tf = raw_rel_tf
    )

    # Downcasting speeds up np.dot
    ICS_tf._grid_vals = 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_tf = engloss_spec(
        eleceng, photeng, T, thomson_tf = raw_engloss_tf, rel_tf = raw_rel_tf
    )

    # Downcasting speeds up np.dot
    engloss_tf._grid_vals = engloss_tf.grid_vals.astype('float64')

    # Define some useful lengths.
    N_eleceng = eleceng.size
    N_photeng = photeng.size

    # Create the secondary electron transfer function.

    sec_elec_tf = tf.TransFuncAtRedshift(
        np.zeros((N_eleceng, N_eleceng)), in_eng = eleceng,
        rs = rs*np.ones_like(eleceng), eng = eleceng,
        dlnz = -1, spec_type = 'dNdE'
    )

    # append_sec_elec_tf = sec_elec_tf.append

    # Change from energy loss spectrum to secondary electron spectrum.
    for i, in_eng in enumerate(eleceng):
        spec = engloss_tf[i]
        spec.engloss_rebin(in_eng, eleceng, fast=True)
        # Add to the appropriate row.
        sec_elec_tf._grid_vals[i] += spec.dNdE


    # 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'
    )
    # Total upscattered photon energy.
    cont_loss_vec = np.zeros_like(eleceng)
    # Deposited energy, enforces energy conservation.
    deposited_vec = np.zeros_like(eleceng)

    # Test input electron to get the spectra.
    delta_spec = np.zeros_like(eleceng)

    # Start building sec_phot_tf and sec_lowengelec_tf.
    # Low energy regime first.

    ####################################
    # OLD: for loop to add identity.   #
    # Not very clever.                 #
    ####################################


    # for i, eng in zip(eleceng_low_ind, eleceng_low):
    #     # Zero out delta function test spectrum, set it correctly
    #     # for the loop ahead.
    #     delta_spec *= 0
    #     delta_spec[i] = 1
    #     # Add the trivial secondary electron spectrum to the
    #     # transfer function.
    #     sec_lowengelec_tf._grid_vals[i] += delta_spec

    ####################################
    # NEW: Just set the relevant       #
    # part to be the identity matrix   #
    ####################################

    sec_lowengelec_tf._grid_vals[:eleceng_low.size, :eleceng_low.size] = (
        np.identity(eleceng_low.size)
    )

    # Continuum energy loss rate, dU_CMB/dt.
    CMB_upscatter_eng_rate = phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)


    # High energy electron loop to get fully resolved spectrum.
    for i, eng in zip(eleceng_high_ind, eleceng_high):

        # print('Check energies and indexing: ')
        # print(i, eleceng[i], eng)

        sec_phot_spec = ICS_tf[i]
        if sec_phot_spec.spec_type == 'dNdE':
            sec_phot_spec.switch_spec_type()

        sec_elec_spec = sec_elec_tf[i]
        if sec_elec_spec.spec_type == 'dNdE':
            sec_elec_spec.switch_spec_type()

        # sec_elec_spec_2 = sec_elec_tf_2[i]
        # if sec_elec_spec_2.spec_type == 'dNdE':
        #     sec_elec_spec_2.switch_spec_type()

        # The total number of primaries scattered is equal to the total number of scattered *photons*.
        # The scattered electrons is obtained from the *net* energy loss, and
        # so is not indicative of number of scatters.
        tot_N_scatter = sec_phot_spec.totN()
        # The total energy of primary electrons which is scattered per unit time.
        tot_eng_scatter = tot_N_scatter*eng
        # The *net* total number of secondary photons produced
        # per unit time.
        sec_elec_N = sec_elec_spec.totN()
        # The *net* total energy of secondary electrons produced
        # per unit time.
        sec_elec_toteng = sec_elec_spec.toteng()
        # The total energy of secondary photons produced per unit time.
        sec_phot_toteng = sec_phot_spec.toteng()
        # Deposited energy per unit time, dD/dt.
        deposited_eng = sec_elec_spec.totN()*eng - sec_elec_toteng - (sec_phot_toteng - CMB_upscatter_eng_rate)

        print('-------- Injection Energy: ', eng)
        print(
            '-------- No. of Scatters (Analytic): ',
            phys.thomson_xsec*phys.c*phys.CMB_N_density(T)
        )
        print(
            '-------- No. of Scatters (Computed): ',
            tot_N_scatter
        )
        gamma_elec = 1 + eng/phys.me
        beta_elec  = np.sqrt(eng/phys.me*(gamma_elec+1)/gamma_elec**2)
        print(
            '-------- Energy lost (Analytic): ',
            (4/3)*phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)*(
                gamma_elec**2 * beta_elec**2
            )
        )
        print(
            '-------- Energy lost (Computed from photons): ',
            engloss_tf[i].toteng()
        )
        print(
            '-------- Energy lost (Computed from electrons): ',
            sec_elec_spec.totN()*eng - sec_elec_toteng
        )
        print(
            '-------- Energy of upscattered photons: ',
            CMB_upscatter_eng_rate
        )
        print(
            '-------- Energy in secondary photons (Computed): ',
            sec_phot_toteng
        )
        print(
            '-------- Energy in secondary photons (Analytic): ',
            phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)*(
                1 + (4/3)* gamma_elec**2 * beta_elec**2
            )
        )
        print(
            '-------- Energy gain from photons: ',
            sec_phot_toteng - CMB_upscatter_eng_rate
        )
        print('-------- Deposited Energy: ', deposited_eng)

        # In the original code, the energy of the electron has gamma > 20,
        # then the continuum energy loss is assigned to deposited_eng instead.
        # I'm not sure if this is necessary, but let's be consistent with the
        # original code for now.

        continuum_engloss = CMB_upscatter_eng_rate

        if eng + phys.me > 20*phys.me:
            deposited_eng -= CMB_upscatter_eng_rate
            continuum_engloss = 0

        # Normalize to one secondary electron.

        sec_phot_spec /= sec_elec_N
        sec_elec_spec /= sec_elec_N
        continuum_engloss /= sec_elec_N
        deposited_eng /= sec_elec_N

        # Remove self-scattering.

        selfscatter_engfrac = (
            sec_elec_spec.N[i]
        )
        scattered_engfrac = 1 - selfscatter_engfrac

        sec_elec_spec.N[i] = 0

        sec_phot_spec /= scattered_engfrac
        sec_elec_spec /= scattered_engfrac
        continuum_engloss /= scattered_engfrac
        deposited_eng /= scattered_engfrac

        # Get the full secondary photon spectrum. Type 'N'
        resolved_phot_spec = sec_phot_tf.sum_specs(sec_elec_spec.N)

        # Get the full secondary low energy electron spectrum. Type 'N'.
        resolved_lowengelec_spec = (
            sec_lowengelec_tf.sum_specs(sec_elec_spec.N)
        )

        # Add the resolved spectrum to the first scatter.
        sec_phot_spec += resolved_phot_spec

        # Resolve the secondary electron continuum loss and deposition.
        continuum_engloss += np.dot(sec_elec_spec.N, cont_loss_vec)

        # utils.compare_arr([sec_elec_spec.N, deposited_vec])
        deposited_eng += np.dot(sec_elec_spec.N, deposited_vec)

        # Now, append the resulting spectrum to the transfer function.
        # Do this without calling append of course: just add to the zeros
        # that fill the current row in _grid_vals.
        sec_phot_tf._grid_vals[i] += sec_phot_spec.N
        sec_lowengelec_tf._grid_vals[i] += resolved_lowengelec_spec.N

        # Set the correct values in cont_loss_vec and deposited_vec.
        cont_loss_vec[i] = continuum_engloss
        deposited_vec[i] = deposited_eng

        # Conservation of energy check. Check that it is 1e-10 of eng.


        conservation_check = (
            eng
            - resolved_lowengelec_spec.toteng()
            + cont_loss_vec[i]
            - sec_phot_spec.toteng()
                         )

        # print('***************************************************')
        # print('injected energy: ', eng)
        # print('low energy e: ', resolved_lowengelec_spec.toteng())
        # print('scattered phot: ', sec_phot_spec.toteng())
        # print('continuum_engloss: ', cont_loss_vec[i])
        # print('diff: ', sec_phot_spec.toteng() - cont_loss_vec[i])
        # print('energy is conserved up to (%): ', conservation_check/eng*100)
        # print('deposited: ', deposited_vec[i])
        # print(
        #     'energy conservation with deposited (%): ',
        #     (conservation_check - deposited_vec[i])/eng*100
        # )
        # print('***************************************************')

        if (
            conservation_check/eng > 0.01
        ):
            print('***************************************************')
            print('rs: ', rs)
            print('injected energy: ', eng)
            print('low energy e: ', resolved_lowengelec_spec.toteng())
            print('scattered phot: ', sec_phot_spec.toteng())
            print('continuum_engloss: ', cont_loss_vec[i])
            print('diff: ', sec_phot_spec.toteng() - cont_loss_vec[i])
            print('energy is conserved up to (%): ', conservation_check/eng*100)
            print('deposited: ', deposited_vec[i])
            print(
                'energy conservation with deposited (%): ',
                (conservation_check - deposited_vec[i])/eng*100
            )
            print('***************************************************')

            raise RuntimeError('Conservation of energy failed.')

        # Force conservation of energy.
        # deposited_vec[i] += conservation_check

    return (sec_phot_tf, sec_lowengelec_tf, cont_loss_vec, deposited_vec)
Example #19
0
    def tla_before_reion(rs, var):
        # Returns an array of values for [dT_DM/dz, dy_be/dz].
        # var = [T_DM, x_be]
        #rs = np.exp(logrs)

        n_D = phys.rho_DM / m_D * rs**3
        #n_D = phys.nH * rs**3 #To match the SM result
        T_D = phys.TCMB(rs) * xi

        def dlogTDM_dz(y_be, log_TDM, rs):
            #rs = np.exp(logrs)

            T_DM = np.exp(log_TDM)
            x_be = get_x(y_be)
            eps = 1 - T_DM / T_D

            # Cooling rate due to adiabatic expansion
            adia = 2 / rs

            #Compton
            comp = phys.dtdz(rs) * norm_compton_cooling_rate(
                x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * (T_D / T_DM - 1.)

            #Rayleigh
            Rayl = phys.dtdz(rs) * Gam_R(x_be, T_DM, rs, alphaD, m_be, m_bp,
                                         xi) * (T_D / T_DM - 1)

            #brem heating + ion heating - recomb cooling
            ff_pi_pr = phys.dtdz(rs) * (
                Gam_ff(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * eps +
                Gam_pi(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) -
                Gam_pr(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) - 0)

            deriv = Rayl + comp + adia + ff_pi_pr

            #Baryon-DM energy exchange
            V_pec = 0
            fDM = 2 * x_be
            xsec = None  #DM baryon scattering cross-section
            #baryon = phys.dtdz(rs) * DM_IGM_cooling_rate(
            #    m_be, m_bp, phys.TCMB(rs), T_DM, V_pec, x_be, rs,
            #    fDM, particle_type='DM', eps=eps
            #)/(3/2 * n_D * (1 + x_be))

            return deriv

        def dybe_dz(y_be, log_T, rs):
            #rs = np.exp(logrs)
            T = np.exp(log_T)
            x_be = get_x(y_be)
            xD = 1 - x_be

            if x_be > 0.999 and rs > 2000:
                # Use the Saha value.
                dxdz = d_x_be_Saha_dz(rs, alphaD, m_be, m_bp, xi)
                return 2 * np.cosh(y_be)**2 * dxdz

            peeb_C = dark_peebles_C(x_be, rs, alphaD, m_be, m_bp, xi)
            alpha = dark_alpha_recomb(T, alphaD, m_be, m_bp, xi)
            beta = dark_beta_ion(T_D, alphaD, m_be, m_bp, xi)
            return 2 * np.cosh(y_be)**2 * phys.dtdz(rs) * (
                -peeb_C *
                (alpha * x_be**2 * n_D - 4 * beta * xD * np.exp(-Lya_D / T_D)))

        nH = phys.nH * rs**3

        def dlogTb_dz(yHII, log_Tb, rs):

            Tb = np.exp(log_Tb)

            xHII = get_x(yHII)
            xHI = 1 - xHII

            adia = 2 / rs
            denom = 3 / 2 * Tb * nH * (1 + chi + xe)
            comp = phys.dtdz(rs) * compton_cooling_rate(xHII, Tb, rs) / denom

            return adia + comp

        log_TDM, y_be = var[0], var[1]

        if both_sectors:
            return 0
        else:
            return np.array(
                [dlogTDM_dz(y_be, log_TDM, rs),
                 dybe_dz(y_be, log_TDM, rs)])
Example #20
0
def get_history(rs_vec,
                init_cond=None,
                alphaD=phys.alpha,
                m_be=phys.me,
                m_bp=phys.mp,
                xi=1,
                both_sectors=False,
                eps=0,
                mxstep=1000,
                rtol=1e-4):
    """Returns the ionization and thermal history of the IGM.

    Parameters
    ----------
    rs_vec : ndarray
        Abscissa for the solution.
    init_cond : array, optional
        Array containing [initial temperature, initial xHII, initial xHeII, initial xHeIII]. Defaults to standard values if None.
    mxstep : int, optional
        The maximum number of steps allowed for each integration point. See *scipy.integrate.odeint* for more information.
    rtol : float, optional
        The relative error of the solution. See *scipy.integrate.odeint* for more information.

    Returns
    -------
    list of ndarray
        [temperature solution (in eV), xHII solution, xHeII, xHeIII].

    Notes
    -----
    The actual differential equation that we solve is expressed in terms of y = arctanh(f*(x - f)), where f = 0.5 for x = xHII, and f = nHe/nH * 0.5 for x = xHeII or xHeIII, where nHe/nH is approximately 0.083.

    """
    B_D = get_BD(alphaD, m_be, m_bp)
    m_D = m_be + m_bp - B_D
    Lya_D = 3 / 4 * B_D

    # Define conversion functions between x and y.
    def get_x(y):
        return 0.5 + 0.5 * np.tanh(y)

    def tla_before_reion(rs, var):
        # Returns an array of values for [dT_DM/dz, dy_be/dz].
        # var = [T_DM, x_be]
        #rs = np.exp(logrs)

        n_D = phys.rho_DM / m_D * rs**3
        #n_D = phys.nH * rs**3 #To match the SM result
        T_D = phys.TCMB(rs) * xi

        def dlogTDM_dz(y_be, log_TDM, rs):
            #rs = np.exp(logrs)

            T_DM = np.exp(log_TDM)
            x_be = get_x(y_be)
            eps = 1 - T_DM / T_D

            # Cooling rate due to adiabatic expansion
            adia = 2 / rs

            #Compton
            comp = phys.dtdz(rs) * norm_compton_cooling_rate(
                x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * (T_D / T_DM - 1.)

            #Rayleigh
            Rayl = phys.dtdz(rs) * Gam_R(x_be, T_DM, rs, alphaD, m_be, m_bp,
                                         xi) * (T_D / T_DM - 1)

            #brem heating + ion heating - recomb cooling
            ff_pi_pr = phys.dtdz(rs) * (
                Gam_ff(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) * eps +
                Gam_pi(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) -
                Gam_pr(x_be, T_DM, rs, alphaD, m_be, m_bp, xi) - 0)

            deriv = Rayl + comp + adia + ff_pi_pr

            #Baryon-DM energy exchange
            V_pec = 0
            fDM = 2 * x_be
            xsec = None  #DM baryon scattering cross-section
            #baryon = phys.dtdz(rs) * DM_IGM_cooling_rate(
            #    m_be, m_bp, phys.TCMB(rs), T_DM, V_pec, x_be, rs,
            #    fDM, particle_type='DM', eps=eps
            #)/(3/2 * n_D * (1 + x_be))

            return deriv

        def dybe_dz(y_be, log_T, rs):
            #rs = np.exp(logrs)
            T = np.exp(log_T)
            x_be = get_x(y_be)
            xD = 1 - x_be

            if x_be > 0.999 and rs > 2000:
                # Use the Saha value.
                dxdz = d_x_be_Saha_dz(rs, alphaD, m_be, m_bp, xi)
                return 2 * np.cosh(y_be)**2 * dxdz

            peeb_C = dark_peebles_C(x_be, rs, alphaD, m_be, m_bp, xi)
            alpha = dark_alpha_recomb(T, alphaD, m_be, m_bp, xi)
            beta = dark_beta_ion(T_D, alphaD, m_be, m_bp, xi)
            return 2 * np.cosh(y_be)**2 * phys.dtdz(rs) * (
                -peeb_C *
                (alpha * x_be**2 * n_D - 4 * beta * xD * np.exp(-Lya_D / T_D)))

        nH = phys.nH * rs**3

        def dlogTb_dz(yHII, log_Tb, rs):

            Tb = np.exp(log_Tb)

            xHII = get_x(yHII)
            xHI = 1 - xHII

            adia = 2 / rs
            denom = 3 / 2 * Tb * nH * (1 + chi + xe)
            comp = phys.dtdz(rs) * compton_cooling_rate(xHII, Tb, rs) / denom

            return adia + comp

        log_TDM, y_be = var[0], var[1]

        if both_sectors:
            return 0
        else:
            return np.array(
                [dlogTDM_dz(y_be, log_TDM, rs),
                 dybe_dz(y_be, log_TDM, rs)])

    if init_cond is None:
        #rs_start = np.exp(logrs_vec[0])
        rs_start = rs_vec[0]
        x_Saha = x_be_Saha(rs_start, alphaD, m_be, m_bp, xi)
        _init_cond = [xi * phys.TCMB(rs_start), x_Saha]

    else:

        _init_cond = np.array(init_cond)

        if init_cond[1] == 1:
            _init_cond[1] = 1 - 1e-12

    _init_cond[0] = np.log(_init_cond[0])
    _init_cond[1] = np.arctanh(2 * (_init_cond[1] - 0.5))
    _init_cond = np.array(_init_cond)

    # Note: no reionization model implemented.
    soln = odeint(tla_before_reion,
                  _init_cond,
                  rs_vec,
                  mxstep=mxstep,
                  tfirst=True,
                  rtol=rtol)
    #soln = rk(
    #    tla_before_reion, _init_cond,
    #    rs_vec[0], rs_vec[10], step=-.01, vec=None,
    #    rtol=1e-2
    #)

    # Convert from log_T_m to T_m
    soln[:, 0] = np.exp(soln[:, 0])
    soln[:, 1] = 0.5 + 0.5 * np.tanh(soln[:, 1])

    return soln
Example #21
0
def get_ics_cooling_tf_fast(
    raw_thomson_tf, raw_rel_tf, raw_engloss_tf,
    eleceng, photeng, rs
):

    """ Transfer function for complete electron cooling through ICS.

    Parameters
    ----------
    raw_thomson_tf : TransFuncAtRedshift
        Raw Thomson ICS scattered photon spectrum transfer function.
    raw_rel_tf : TransFuncAtRedshift
        Raw relativistic ICS scattered photon spectrum transfer function.
    raw_engloss_tf : TransFuncAtRedshift
        Raw Thomson ICS scattered electron net energy loss spectrum transfer function.
    eleceng : ndarray
        The electron *kinetic* energy abscissa.
    photeng : ndarray
        The photon energy abscissa.
    rs : float
        The redshift (1+z). 

    Returns
    -------

    tuple of TransFuncAtRedshift
        Transfer functions for photons and low energy electrons.

    Notes
    -----
    The raw transfer functions should be generated when the code package is first installed. The transfer function corresponds to the fully resolved
    photon spectrum after scattering by one electron.

    This version of the code works faster, but dispenses with energy conservation checks and several other safeguards. Use only with default abscissa, or when get_ics_cooling_tf works.

    """

    T = phys.TCMB(rs)

    # Photon transfer function for single primary electron single scattering.
    # This is dN/(dE dt), dt = 1 s.
    ICS_tf = ics_spec(
        eleceng, photeng, T, thomson_tf = raw_thomson_tf, rel_tf = raw_rel_tf
    )

    # Downcasting speeds up np.dot
    ICS_tf._grid_vals = 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_tf = engloss_spec(
        eleceng, photeng, T, thomson_tf = raw_engloss_tf, rel_tf = raw_rel_tf
    )

    # Downcasting speeds up np.dot
    engloss_tf._grid_vals = engloss_tf.grid_vals.astype('float64')

    # Switch the spectra type here to type 'N'.
    if ICS_tf.spec_type == 'dNdE':
        ICS_tf.switch_spec_type()
    if engloss_tf.spec_type == 'dNdE':
        engloss_tf.switch_spec_type()


    # Define some useful lengths.
    N_eleceng = eleceng.size
    N_photeng = photeng.size

    # Create the secondary electron transfer function.

    sec_elec_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'
    )

    sec_elec_tf._grid_vals = spectools.engloss_rebin_fast(
        eleceng, photeng, engloss_tf.grid_vals, eleceng
    )

    # Change from energy loss spectrum to secondary electron spectrum.
    # for i, in_eng in enumerate(eleceng):
    #     spec = engloss_tf[i]
    #     spec.engloss_rebin(
    #         in_eng, eleceng, out_spec_type='N', fast=True
    #     )
    #     # Add to the appropriate row.
    #     sec_elec_tf._grid_vals[i] += spec.N

    # 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'
    )
    # Total upscattered photon energy.
    cont_loss_vec = np.zeros_like(eleceng)
    # Deposited energy, enforces energy conservation.
    deposited_vec = np.zeros_like(eleceng)

    # Test input electron to get the spectra.
    delta_spec = np.zeros_like(eleceng)

    # Start building sec_phot_tf and sec_lowengelec_tf.
    # Low energy regime first.

    sec_lowengelec_tf._grid_vals[:eleceng_low.size, :eleceng_low.size] = (
        np.identity(eleceng_low.size)
    )

    # Continuum energy loss rate, dU_CMB/dt.
    CMB_upscatter_eng_rate = phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)


    # High energy electron loop to get fully resolved spectrum.
    for i, eng in zip(eleceng_high_ind, eleceng_high):

        # print('Check energies and indexing: ')
        # print(i, eleceng[i], eng)

        sec_phot_spec_N = ICS_tf._grid_vals[i]

        sec_elec_spec_N = sec_elec_tf._grid_vals[i]

        # The total number of primaries scattered is equal to the total number of scattered *photons*.
        # The scattered electrons is obtained from the *net* energy loss, and
        # so is not indicative of number of scatters.
        tot_N_scatter = np.sum(sec_phot_spec_N)
        # The total energy of primary electrons which is scattered per unit time.
        tot_eng_scatter = tot_N_scatter*eng
        # The *net* total number of secondary photons produced
        # per unit time.
        sec_elec_totN = np.sum(sec_elec_spec_N)
        # The *net* total energy of secondary electrons produced
        # per unit time.
        sec_elec_toteng = np.dot(sec_elec_spec_N, eleceng)
        # The total energy of secondary photons produced per unit time.
        sec_phot_toteng = np.dot(sec_phot_spec_N, photeng)
        # Deposited energy per unit time, dD/dt.
        # Numerical error (should be zero except for numerics)
        deposited_eng = sec_elec_totN*eng - sec_elec_toteng - (sec_phot_toteng - CMB_upscatter_eng_rate)

        diagnostics = False

        if diagnostics:
            print('-------- Injection Energy: ', eng)
            print(
                '-------- No. of Scatters (Analytic): ',
                phys.thomson_xsec*phys.c*phys.CMB_N_density(T)
            )
            print(
                '-------- No. of Scatters (Computed): ',
                tot_N_scatter
            )
            gamma_elec = 1 + eng/phys.me
            beta_elec  = np.sqrt(eng/phys.me*(gamma_elec+1)/gamma_elec**2)
            print(
                '-------- Energy lost (Analytic): ',
                (4/3)*phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)*(
                    gamma_elec**2 * beta_elec**2
                )
            )
            print(
                '-------- Energy lost (Computed from photons): ',
                engloss_tf[i].toteng()
            )
            print(
                '-------- Energy lost (Computed from electrons): ',
                sec_elec_totN*eng - sec_elec_toteng
            )
            print(
                '-------- Energy of upscattered photons: ',
                CMB_upscatter_eng_rate
            )
            print(
                '-------- Energy in secondary photons (Computed): ',
                sec_phot_toteng
            )
            print(
                '-------- Energy in secondary photons (Analytic): ',
                phys.thomson_xsec*phys.c*phys.CMB_eng_density(T)*(
                    1 + (4/3)* gamma_elec**2 * beta_elec**2
                )
            )
            print(
                '-------- Energy gain from photons: ',
                sec_phot_toteng - CMB_upscatter_eng_rate
            )
            print('-------- Deposited Energy: ', deposited_eng)


        # In the original code, the energy of the electron has gamma > 20,
        # then the continuum energy loss is assigned to deposited_eng instead.
        # I'm not sure if this is necessary, but let's be consistent with the
        # original code for now.

        continuum_engloss = CMB_upscatter_eng_rate

        if eng + phys.me > 20*phys.me:
            deposited_eng += CMB_upscatter_eng_rate
            continuum_engloss = 0

        # Normalize to one secondary electron.

        sec_phot_spec_N /= sec_elec_totN
        sec_elec_spec_N /= sec_elec_totN
        continuum_engloss /= sec_elec_totN
        deposited_eng /= sec_elec_totN

        # Remove self-scattering.

        selfscatter_engfrac = (
            sec_elec_spec_N[i]
        )
        scattered_engfrac = 1 - selfscatter_engfrac

        sec_elec_spec_N[i] = 0

        sec_phot_spec_N /= scattered_engfrac
        sec_elec_spec_N /= scattered_engfrac
        continuum_engloss /= scattered_engfrac
        deposited_eng /= scattered_engfrac

        # Get the full secondary photon spectrum. Type 'N'
        resolved_phot_spec_vals = np.dot(
            sec_elec_spec_N, sec_phot_tf._grid_vals
        )
        # Get the full secondary low energy electron spectrum. Type 'N'.

        # resolved_lowengelec_spec_vals = np.dot(
        #     sec_elec_spec_N, sec_lowengelec_tf._grid_vals
        # )

        # The resolved lowengelec spectrum is simply one electron
        # in the bin just below 3 keV.
        # Added directly to sec_lowengelec_tf. Removed the dot for speed.
        # resolved_lowengelec_spec_vals = np.zeros_like(eleceng)
        # resolved_lowengelec_spec_vals[eleceng_low_ind[-1]] += 1

        # Add the resolved spectrum to the first scatter.
        sec_phot_spec_N += resolved_phot_spec_vals

        # Resolve the secondary electron continuum loss and deposition.
        continuum_engloss += np.dot(sec_elec_spec_N, cont_loss_vec)

        deposited_eng += np.dot(sec_elec_spec_N, deposited_vec)

        # Now, append the resulting spectrum to the transfer function.
        # Do this without calling append of course: just add to the zeros
        # that fill the current row in _grid_vals.
        sec_phot_tf._grid_vals[i] += sec_phot_spec_N
        sec_lowengelec_tf._grid_vals[i, eleceng_low_ind[-1]] += 1
        # Set the correct values in cont_loss_vec and deposited_vec.
        cont_loss_vec[i] = continuum_engloss
        deposited_vec[i] = deposited_eng

        check = False

        if check:

            conservation_check = (
                eng
                - np.dot(resolved_lowengelec_spec_vals, eleceng)
                + cont_loss_vec[i]
                - np.dot(sec_phot_spec_N, photeng)
            )

            if (
                conservation_check/eng > 0.01
            ):
                print('***************************************************')
                print('rs: ', rs)
                print('injected energy: ', eng)
                print(
                    'low energy e: ',
                    np.dot(resolved_lowengelec_spec_vals, eleceng)
                )
                print('scattered phot: ', np.dot(sec_phot_spec_N, photeng))
                print('continuum_engloss: ', cont_loss_vec[i])
                print(
                    'diff: ',
                    np.dot(sec_phot_spec_N, photeng) - cont_loss_vec[i]
                )
                print(
                    'energy is conserved up to (%): ',
                    conservation_check/eng*100
                )
                print('deposited: ', deposited_vec[i])
                print(
                    'energy conservation with deposited (%): ',
                    (conservation_check - deposited_vec[i])/eng*100
                )
                print('***************************************************')

                raise RuntimeError('Conservation of energy failed.')


    return (sec_phot_tf, sec_lowengelec_tf, cont_loss_vec, deposited_vec)
Example #22
0
def compton_cooling_rate(xHII, T_m, rs):
    """SM Compton cooling rate. dE/dVdt"""
    ne = xHII * phys.nH * rs**3
    pre = 4 * phys.thomson_xsec * 4 * phys.stefboltz / phys.me
    return pre * (phys.TCMB(rs) - T_m) * phys.TCMB(rs)**4