예제 #1
0
    def _normalize(_phi, _k1, _k2, _r, _mu):
        # First calculate the sound speed, radius at the sonic point and the
        # density at the sonic point. They will be useful to change the units of
        # the calculation aiming to avoid numerical overflows
        _vs = parker.sound_speed(temperature, _mu)
        _rs = parker.radius_sonic_point(planet_mass, _vs)
        _rhos = parker.density_sonic_point(mass_loss_rate, _rs, _vs)
        # And now normalize everything
        phi_unit = _vs * 1E5 / _rs / 7.1492E+09  # 1 / s
        phi_norm = _phi / phi_unit
        k1_unit = 1 / (_rhos * _rs * 7.1492E+09)  # cm ** 2 / g
        k1_norm = _k1 / k1_unit
        k2_unit = _vs * 1E5 / _rs / 7.1492E+09 / _rhos  # cm ** 3 / g / s
        k2_norm = _k2 / k2_unit
        r_norm = (_r * planet_radius / _rs)

        # The differential r will be useful at some point
        dr_norm = np.diff(r_norm)
        dr_norm = np.concatenate((dr_norm, np.array([
            dr_norm[-1],
        ])))

        # The structure of the atmosphere
        v_norm, rho_norm = parker.structure(r_norm)

        return phi_norm, k1_norm, k2_norm, r_norm, dr_norm, v_norm, rho_norm
예제 #2
0
    def _fun(_r, _f, _phi, _k2):
        if exact_phi:
            _phi_prime = _phi_prime_fun(np.array([
                _r,
            ]))[0]
        else:
            _t = _tau_fun(np.array([
                _r,
            ]))[0]
            _phi_prime = np.exp(-_t) * _phi
        _v_guess = _v_fun(_r)
        _v, _rho = parker.structure(_r, _v_guess)
        # In terms 1 and 2 we use the values of k2 and phi from above
        term1 = (1. - _f) / _v * _phi_prime
        term2 = _k2 * _rho * _f**2 / _v
        df_dr = term1 - term2

        return df_dr
예제 #3
0
def test_structure(r=1.0, precision_threshold=1E-6):
    velocity, density = parker.structure(r)
    assert abs((velocity - 1.0) / velocity) < precision_threshold
    assert abs((density - 1.0) / density) < precision_threshold
예제 #4
0
def ion_fraction(radius_profile,
                 planet_radius,
                 temperature,
                 h_fraction,
                 mass_loss_rate,
                 planet_mass,
                 mean_molecular_weight_0=1.0,
                 spectrum_at_planet=None,
                 flux_euv=None,
                 initial_f_ion=0.0,
                 relax_solution=False,
                 convergence=0.01,
                 max_n_relax=10,
                 exact_phi=False,
                 return_mu=False,
                 **options_solve_ivp):
    """
    Calculate the fraction of ionized hydrogen in the upper atmosphere in
    function of the radius in unit of planetary radius.

    Parameters
    ----------
    radius_profile (``numpy.ndarray``):
        Radius in unit of planetary radii.

    planet_radius (``float``):
        Planetary radius in unit of Jupiter radius.

    temperature (``float``):
        Isothermal temperature of the upper atmosphere in unit of Kelvin.

    h_fraction (``float``):
        Total (ion + neutral) H number fraction of the atmosphere.

    mass_loss_rate (``float``):
        Mass loss rate of the planet in units of g / s.

    planet_mass (``float``):
        Planetary mass in unit of Jupiter mass.

    mean_molecular_weight_0 (``float``):
        Initial mean molecular weight of the atmosphere in unit of proton mass.
        Default value is 1.0 (100% neutral H). Since its final value depend on
        the H ion fraction itself, the mean molecular weight can be
        self-consistently calculated by setting `relax_solution` to `True`.

    spectrum_at_planet (``dict``, optional):
        Spectrum of the host star arriving at the planet covering fluxes at
        least up to the wavelength corresponding to the energy to ionize
        hydrogen (13.6 eV, or 911.65 Angstrom). Can be generated using
        ``tools.make_spectrum_dict``. If ``None``, then ``flux_euv`` must be
        provided instead. Default is ``None``.

    flux_euv (``float``, optional):
        Extreme-ultraviolet (0-911.65 Angstrom) flux arriving at the planet in
        units of erg / s / cm ** 2. If ``None``, then ``spectrum_at_planet``
        must be provided instead. Default is ``None``.

    initial_f_ion (``float``, optional):
        The initial ionization fraction at the layer near the surface of the
        planet. Default is 0.0, i.e., 100% neutral.

    relax_solution (``bool``, optional):
        The first solution is calculating by initially assuming the entire
        atmosphere is in neutral state. If ``True``, the solution will be
        re-calculated in a loop until it converges to a delta_f of 1%, or for a
        maximum of 10 loops (default parameters). Default is ``False``.

    convergence (``float``, optional):
        Value of delta_f at which to stop the relaxation of the solution for
        ``f_r``. Default is 0.01.

    max_n_relax (``int``, optional):
        Maximum number of loops to perform the relaxation of the solution for
        ``f_r``. Default is 10.

    return_mu (``bool``, optional):
        If ``True``, then this function returns a second variable ``mu_bar``,
        which is the self-consistent, density-averaged mean molecular weight of
        the atmosphere. Equivalent to the ``mu_bar`` of Eq. A.3 in Lampón et
        al. 2020.

    **options_solve_ivp:
        Options to be passed to the ``scipy.integrate.solve_ivp()`` solver. You
        may want to change the options ``method`` (integration method; default
        is ``'RK45'``), ``atol`` (absolute tolerance; default is 1E-6) or
        ``rtol`` (relative tolerance; default is 1E-3). If you are having
        numerical issues, you may want to decrease the tolerance by a factor of
        10 or 100, or 1000 in extreme cases.

    Returns
    -------
    f_r (``numpy.ndarray``):
        Values of the fraction of ionized hydrogen in function of the radius.

    mu_bar (``float``):
        Mean molecular weight of the atmosphere, in unit of proton mass,
        averaged across the radial distance using according to the function
        `average_molecular_weight` in the `parker` module. Only returned when
        ``return_mu`` is set to ``True``.
    """
    # Hydrogen recombination rate
    alpha_rec = recombination(temperature)

    # Hydrogen mass in g
    m_h = 1.67262192E-24

    # Photoionization rate at null optical depth at the distance of the planet
    # from the host star, in unit of 1 / s.
    if exact_phi and spectrum_at_planet is not None:
        vs = parker.sound_speed(temperature, mean_molecular_weight_0)
        rs = parker.radius_sonic_point(planet_mass, vs)
        rhos = parker.density_sonic_point(mass_loss_rate, rs, vs)
        _, rho_norm = parker.structure(radius_profile * planet_radius / rs)
        f_outer = 0.0  # Assume completely ionized at the top of atmosphere
        phi_abs = radiative_processes_exact(
            spectrum_at_planet,
            (radius_profile * planet_radius * u.Rjup).to(u.cm).value,
            rho_norm * rhos, f_outer, h_fraction)
        a_0 = 0.
    elif spectrum_at_planet is not None:
        phi_abs, a_0 = radiative_processes(spectrum_at_planet)
    elif flux_euv is not None:
        phi_abs, a_0 = radiative_processes_mono(flux_euv)
    else:
        raise ValueError('Either `spectrum_at_planet` or `flux_euv` must be '
                         'provided.')

    # Multiplicative factor of Eq. 11 of Oklopcic & Hirata 2018, unit of
    # cm ** 2 / g
    # We assume that the remaining of the number fraction is pure He
    he_fraction = 1 - h_fraction
    he_h_fraction = he_fraction / h_fraction
    k1_abs = h_fraction * a_0 / (h_fraction + 4 * he_fraction) / m_h

    # Multiplicative factor of the second term in the right-hand side of Eq.
    # 13 of Oklopcic & Hirata 2018, unit of cm ** 3 / s / g
    k2_abs = h_fraction / (h_fraction + 4 * he_fraction) * alpha_rec / m_h

    # In order to avoid numerical overflows, we need to normalize a few key
    # variables. Since the normalization may need to be repeated to relax the
    # solution, we have a function to do it.
    def _normalize(_phi, _k1, _k2, _r, _mu):
        # First calculate the sound speed, radius at the sonic point and the
        # density at the sonic point. They will be useful to change the units of
        # the calculation aiming to avoid numerical overflows
        _vs = parker.sound_speed(temperature, _mu)
        _rs = parker.radius_sonic_point(planet_mass, _vs)
        _rhos = parker.density_sonic_point(mass_loss_rate, _rs, _vs)
        # And now normalize everything
        phi_unit = _vs * 1E5 / _rs / 7.1492E+09  # 1 / s
        phi_norm = _phi / phi_unit
        k1_unit = 1 / (_rhos * _rs * 7.1492E+09)  # cm ** 2 / g
        k1_norm = _k1 / k1_unit
        k2_unit = _vs * 1E5 / _rs / 7.1492E+09 / _rhos  # cm ** 3 / g / s
        k2_norm = _k2 / k2_unit
        r_norm = (_r * planet_radius / _rs)

        # The differential r will be useful at some point
        dr_norm = np.diff(r_norm)
        dr_norm = np.concatenate((dr_norm, np.array([
            dr_norm[-1],
        ])))

        # The structure of the atmosphere
        v_norm, rho_norm = parker.structure(r_norm)

        return phi_norm, k1_norm, k2_norm, r_norm, dr_norm, v_norm, rho_norm

    phi, k1, k2, r, dr, velocity, density = _normalize(
        phi_abs, k1_abs, k2_abs, radius_profile, mean_molecular_weight_0)

    if exact_phi:
        _phi_prime_fun = interp1d(r, phi, fill_value="extrapolate")
    else:
        # To start the calculations we need the optical depth, but technically
        # we don't know it yet, because it depends on the ion fraction in the
        # atmosphere, which is what we want to obtain. However, the optical
        # depth depends more strongly on the densities of H than the ion
        # fraction, so a good approximation is to assume the whole atmosphere is
        # neutral at first.
        column_density = np.flip(np.cumsum(np.flip(dr * density)))
        tau_initial = k1 * column_density
        # We do a dirty hack to make tau_initial and velocity a callable
        # function so it's easily parsed inside the differential equation solver
        _tau_fun = interp1d(r, tau_initial, fill_value="extrapolate")
    _v_fun = interp1d(r, velocity, fill_value="extrapolate")

    # Now let's solve the differential eq. 13 of Oklopcic & Hirata 2018
    # The differential equation in function of r
    def _fun(_r, _f, _phi, _k2):
        if exact_phi:
            _phi_prime = _phi_prime_fun(np.array([
                _r,
            ]))[0]
        else:
            _t = _tau_fun(np.array([
                _r,
            ]))[0]
            _phi_prime = np.exp(-_t) * _phi
        _v_guess = _v_fun(_r)
        _v, _rho = parker.structure(_r, _v_guess)
        # In terms 1 and 2 we use the values of k2 and phi from above
        term1 = (1. - _f) / _v * _phi_prime
        term2 = _k2 * _rho * _f**2 / _v
        df_dr = term1 - term2

        return df_dr

    # We solve it using `scipy.solve_ivp`
    sol = solve_ivp(_fun, (
        r[0],
        r[-1],
    ),
                    np.array([
                        initial_f_ion,
                    ]),
                    t_eval=r,
                    args=(phi, k2),
                    **options_solve_ivp)
    f_r = sol['y'][0]

    # When `solve_ivp` has problems, it may return an array with different
    # size than `r`. So we raise an exception if this happens
    if len(f_r) != len(r):
        raise RuntimeError('The solver ``solve_ivp`` failed to obtain a'
                           ' solution.')

    # Calculate the average mean molecular weight using Eq. A.3 from Lampón et
    # al. 2020
    mu_bar = parker.average_molecular_weight(f_r, radius_profile, velocity,
                                             planet_mass, temperature,
                                             he_h_fraction)

    # For the sake of self-consistency, there is the option of repeating the
    # calculation of f_r by updating the optical depth with the new ion
    # fractions.
    if relax_solution is True:
        for i in range(max_n_relax):
            previous_f_r = np.copy(f_r)

            if exact_phi:
                # phi_abs will need to be recomputed here with the new density
                # structure
                vs = parker.sound_speed(temperature, mu_bar)
                rs = parker.radius_sonic_point(planet_mass, vs)
                rhos = parker.density_sonic_point(mass_loss_rate, rs, vs)
                _, rho_norm = parker.structure(radius_profile * planet_radius /
                                               rs)
                phi_abs = radiative_processes_exact(
                    spectrum_at_planet,
                    (radius_profile * planet_radius * u.Rjup).to(u.cm).value,
                    rho_norm * rhos, f_r, h_fraction)

            # We re-normalize key parameters because the newly-calculated f_ion
            # changes the value of the mean molecular weight of the atmosphere
            phi, k1, k2, r, dr, velocity, density = _normalize(
                phi_abs, k1_abs, k2_abs, radius_profile, mu_bar)

            if exact_phi:
                _phi_prime_fun = interp1d(r, phi, fill_value="extrapolate")
            else:
                # Re-calculate the column densities
                column_density = np.flip(
                    np.cumsum(np.flip(dr * density * (1 - f_r))))
                tau = k1 * column_density
                _tau_fun = interp1d(r, tau, fill_value="extrapolate")
            _v_fun = interp1d(r, velocity, fill_value="extrapolate")

            # And solve it again
            sol = solve_ivp(_fun, (
                r[0],
                r[-1],
            ),
                            np.array([
                                initial_f_ion,
                            ]),
                            t_eval=r,
                            args=(phi, k2),
                            **options_solve_ivp)
            f_r = sol['y'][0]

            # Raise an error if the length of `f_r` is different from the length
            # of `r`
            if len(f_r) != len(r):
                raise RuntimeError(
                    'The solver ``solve_ivp`` failed to obtain a'
                    ' solution.')

            # Here we update the average mean molecular weight
            mu_bar = parker.average_molecular_weight(f_r, radius_profile,
                                                     velocity, planet_mass,
                                                     temperature,
                                                     he_h_fraction)

            # Calculate the relative change of f_ion in the outer shell of the
            # atmosphere (where we expect the most important change)
            # relative_delta_f = abs(f_r[-1] - previous_f_r_outer_layer) \
            #     / previous_f_r_outer_layer
            relative_delta_f = abs(
                np.sum(f_r - previous_f_r) / np.sum(previous_f_r))

            # Break the loop if convergence is achieved
            if relative_delta_f < convergence:
                break
            else:
                pass
    else:
        pass

    if return_mu is False:
        return f_r
    else:
        return f_r, mu_bar
예제 #5
0
def test_population_fraction_spectrum():
    units = {
        'wavelength': u.angstrom,
        'flux': u.erg / u.s / u.cm**2 / u.angstrom
    }
    spectrum = tools.make_spectrum_from_file(data_test_url, units)

    # First calculate the hydrogen ion fraction
    f_r, mu_bar = hydrogen.ion_fraction(r,
                                        R_pl,
                                        T_0,
                                        h_fraction,
                                        m_dot,
                                        M_pl,
                                        average_mu,
                                        spectrum_at_planet=spectrum,
                                        relax_solution=True,
                                        exact_phi=True,
                                        return_mu=True)

    # Calculate the structure
    vs = parker.sound_speed(T_0, mu_bar)  # Speed of sound (km/s, assumed to be
    # constant)
    rs = parker.radius_sonic_point(
        M_pl, vs)  # Radius at the sonic point (jupiterRad)
    rhos = parker.density_sonic_point(
        m_dot, rs, vs)  # Density at the sonic point (g/cm^3)

    # Some useful arrays for the modeling
    r_array = r * R_pl / rs  # Radius in unit of radius at
    # sonic point
    v_array, rho_array = parker.structure(r_array)

    # Now calculate the population of helium
    f_he_1_odeint, f_he_3_odeint = helium.population_fraction(
        r,
        v_array,
        rho_array,
        f_r,
        R_pl,
        T_0,
        h_fraction,
        vs,
        rs,
        rhos,
        spectrum_at_planet=spectrum,
        initial_state=initial_state,
        relax_solution=True)

    # Assert if all values of the fractions are between 0 and 1
    n_neg = len(np.where(f_he_1_odeint < 0)[0]) + \
        len(np.where(f_he_3_odeint < 0)[0])
    n_one = len(np.where(f_he_1_odeint > 1)[0]) + \
        len(np.where(f_he_3_odeint > 1)[0])
    assert n_neg == 0
    assert n_one == 0

    f_he_1_ivp, f_he_3_ivp = helium.population_fraction(
        r,
        v_array,
        rho_array,
        f_r,
        R_pl,
        T_0,
        h_fraction,
        vs,
        rs,
        rhos,
        spectrum_at_planet=spectrum,
        initial_state=initial_state,
        relax_solution=True,
        method='Radau',
        atol=1E-8,
        rtol=1E-8)

    # Assert if all values of the fractions are between 0 and 1
    n_neg = len(np.where(f_he_1_ivp < 0)[0]) + \
        len(np.where(f_he_3_ivp < 0)[0])
    n_one = len(np.where(f_he_1_ivp > 1)[0]) + \
        len(np.where(f_he_3_ivp > 1)[0])
    assert n_neg == 0
    assert n_one == 0