Source code for species.read.read_radtrans

"""
Module for generating atmospheric model spectra with ``petitRADTRANS``.
Details on the radiative transfer, atmospheric setup, and opacities
can be found in `Mollière et al. (2019) <https://ui.adsabs.harvard.edu
/abs/2019A%26A...627A..67M/abstract>`_.
"""

import warnings

from typing import Dict, List, Optional, Tuple, Union

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import spectres

from matplotlib.ticker import MultipleLocator
from PyAstronomy.pyasl import fastRotBroad
from scipy.interpolate import interp1d
from typeguard import typechecked

from species.core import constants
from species.core.box import ModelBox, create_box
from species.phot.syn_phot import SyntheticPhotometry
from species.read.read_filter import ReadFilter
from species.util.convert_util import logg_to_mass
from species.util.dust_util import apply_ism_ext
from species.util.retrieval_util import (
    calc_metal_ratio,
    calc_spectrum_clear,
    calc_spectrum_clouds,
    convolve_spectrum,
    log_x_cloud_base,
    pt_ret_model,
    pt_spline_interp,
    quench_pressure,
    scale_cloud_abund,
)


[docs] class ReadRadtrans: """ Class for generating a model spectrum with ``petitRADTRANS``. """ @typechecked def __init__( self, line_species: Optional[List[str]] = None, cloud_species: Optional[List[str]] = None, scattering: bool = False, wavel_range: Optional[Tuple[float, float]] = None, filter_name: Optional[str] = None, pressure_grid: str = "smaller", res_mode: str = "c-k", cloud_wavel: Optional[Tuple[float, float]] = None, max_press: Optional[float] = None, pt_manual: Optional[np.ndarray] = None, lbl_opacity_sampling: Optional[Union[int, np.int_]] = None, ) -> None: """ Parameters ---------- line_species : list, None List with the line species. No line species are used if set to ``None``. cloud_species : list, None List with the cloud species. No clouds are used if set to ``None``. scattering : bool Include scattering in the radiative transfer. wavel_range : tuple(float, float), None Wavelength range (:math:`\\mu`m). The wavelength range is set to 0.8-10.0 :math:`\\mu`m if set to ``None`` or not used if ``filter_name`` is not ``None``. filter_name : str, None Filter name that is used for the wavelength range. The ``wavel_range`` is used if ``filter_name`` is set to ``None``. pressure_grid : str The type of pressure grid that is used for the radiative transfer. Either 'standard', to use 180 layers both for the atmospheric structure (e.g. when interpolating the abundances) and 180 layers with the radiative transfer, or 'smaller' to use 60 (instead of 180) with the radiative transfer, or 'clouds' to start with 1440 layers but resample to ~100 layers (depending on the number of cloud species) with a refinement around the cloud decks. For cloudless atmospheres it is recommended to use 'smaller', which runs faster than 'standard' and provides sufficient accuracy. For cloudy atmosphere, one can test with 'smaller' but it is recommended to use 'clouds' for improved accuracy fluxes. res_mode : str Resolution mode ('c-k' or 'lbl'). The low-resolution mode ('c-k') calculates the spectrum with the correlated-k assumption at :math:`\\lambda/\\Delta \\lambda = 1000`. The high-resolution mode ('lbl') calculates the spectrum with a line-by-line treatment at :math:`\\lambda/\\Delta \\lambda = 10^6`. cloud_wavel : tuple(float, float), None Tuple with the wavelength range (:math:`\\mu`m) that is used for calculating the median optical depth of the clouds at the gas-only photosphere and then scaling the cloud optical depth to the value of ``log_tau_cloud``. The range of ``cloud_wavel`` should be encompassed by the range of ``wavel_range``. The full wavelength range (i.e. ``wavel_range``) is used if the argument is set to ``None``. max_pressure : float, None Maximum pressure (bar) for the free temperature nodes. The default value is set to 1000 bar. pt_manual : np.ndarray, None A 2D array that contains the P-T profile that is used when ``pressure_grid="manual"``. The shape of array should be (n_pressure, 2), with pressure (bar) as first column and temperature (K) as second column. It is recommended that the pressures are logarithmically spaced. lbl_opacity_sampling : int, None This is the same parameter as in ``petitRADTRANS`` which is used with ``res_mode='lbl'`` to downsample the line-by-line opacities by selecting every ``lbl_opacity_sampling``-th wavelength from the original sampling of :math:`\\lambda/\\Delta \\lambda = 10^6`. Setting this parameter will lower the computation time. By setting the argument to ``None``, the original sampling is used so no downsampling is applied. Returns ------- NoneType None """ # Set several of the required ReadRadtrans attributes self.filter_name = filter_name self.wavel_range = wavel_range self.scattering = scattering self.pressure_grid = pressure_grid self.cloud_wavel = cloud_wavel self.pt_manual = pt_manual self.lbl_opacity_sampling = lbl_opacity_sampling # Set maximum pressure if max_press is None: self.max_press = 1e3 else: self.max_press = max_press # Set the wavelength range if self.filter_name is not None: transmission = ReadFilter(self.filter_name) self.wavel_range = transmission.wavelength_range() self.wavel_range = (0.9 * self.wavel_range[0], 1.2 * self.wavel_range[1]) elif self.wavel_range is None: self.wavel_range = (0.8, 10.0) # Set the list with line species if line_species is None: self.line_species = [] else: self.line_species = line_species # Set the list with cloud species and the number of P-T points if cloud_species is None: self.cloud_species = [] else: self.cloud_species = cloud_species # Set the number of pressures if self.pressure_grid in ["standard", "smaller"]: # Initiate 180 pressure layers but use only # 60 layers during the radiative transfer # when pressure_grid is set to 'smaller' n_pressure = 180 elif self.pressure_grid == "clouds": # Initiate 1140 pressure layers but use fewer # layers (~100) during the radiative tranfer # after running make_half_pressure_better n_pressure = 1440 else: raise ValueError( f"The argument of pressure_grid " f"('{self.pressure_grid}') is " f"not recognized. Please use " f"'standard', 'smaller', or 'clouds'." ) # Create 180 pressure layers in log space if self.pressure_grid == "manual": if self.pt_manual is None: raise UserWarning( "A 2D array with the P-T profile " "should be provided as argument " "of pt_manual when using " "pressure_grid='manual'." ) self.pressure = self.pt_manual[:, 0] else: self.pressure = np.logspace(-6, np.log10(self.max_press), n_pressure) # Import petitRADTRANS here because it is slow print("Importing petitRADTRANS...", end="", flush=True) from petitRADTRANS.radtrans import Radtrans print(" [DONE]") # Create the Radtrans object self.rt_object = Radtrans( line_species=self.line_species, rayleigh_species=["H2", "He"], cloud_species=self.cloud_species, continuum_opacities=["H2-H2", "H2-He"], wlen_bords_micron=self.wavel_range, mode=res_mode, test_ck_shuffle_comp=self.scattering, do_scat_emis=self.scattering, lbl_opacity_sampling=lbl_opacity_sampling, ) # Setup the opacity arrays if self.pressure_grid == "standard": self.rt_object.setup_opa_structure(self.pressure) elif self.pressure_grid == "manual": self.rt_object.setup_opa_structure(self.pressure) elif self.pressure_grid == "smaller": self.rt_object.setup_opa_structure(self.pressure[::3]) elif self.pressure_grid == "clouds": self.rt_object.setup_opa_structure(self.pressure[::24]) # Set the default of abund_smooth to None self.abund_smooth = None
[docs] @typechecked def get_model( self, model_param: Dict[str, float], quenching: Optional[str] = None, spec_res: Optional[float] = None, wavel_resample: Optional[np.ndarray] = None, plot_contribution: Optional[Union[bool, str]] = False, temp_nodes: Optional[Union[int, np.integer]] = None, ) -> ModelBox: """ Function for calculating a model spectrum with radiative transfer code of ``petitRADTRANS``. Parameters ---------- model_param : dict Dictionary with the model parameters. Various parameterizations can be used for the pressure-temperature (P-T) profile, abundances (chemical equilibrium or free abundances), and the cloud properties. The type of parameterizations that will be used depend on the parameters provided in the dictionary of ``model_param``. Below is an (incomplete) list of the supported parameters. Mandatory parameters: - The surface gravity, ``logg``, should always be included. It is provided in cgs units as :math:`\\log_{10}{g}`. Scaling parameters (optional): - The radius (:math:`R_\\mathrm{J}`), ``radius``, and parallax (mas), ``parallax``, are optional parameters that can be included for scaling the flux from the planet surface to the observer. - Instead of ``parallax``, it is also possible to provided the distance (pc) with the ``distance`` parameter. Chemical abundances (mandatory -- one of the options should be used): - Chemical equilibrium requires the ``metallicity``, ``c_o_ratio`` parameters. Optionally, the ``log_p_quench`` (as :math:`\\log_{10}P/\\mathrm{bar}`) can be included for setting a quench pressure for CO/CH$_4$/H$_2$O. If this last parameter is used, then the argument of ``quenching`` should be set to ``'pressure'``. - Free abundances requires the parameters that have the names from ``line_species`` and ``cloud_species``. These will be used as :math:`\\log_{10}` mass fraction of the line and cloud species. For example, if ``line_species`` includes ``H2O_HITEMP`` then ``model_param`` should contain the ``H2O_HITEMP`` parameter. For a mass fraction of :math:`10^{-3}` the dictionary value can be set to -3. Or, if ``cloud_species`` contains ``MgSiO3(c)_cd`` then ``model_param`` should contain the ``MgSiO3(c)`` parameter. So it is provided without the suffix, ``_cd``, for the particle shape and structure. Pressure-temperature (P-T) profiles (mandatory -- one of the options should be used): - Eddington approximation requires the ``tint`` and ``log_delta`` parameters. - Parametrization from `Mollière et al (2020) <https://ui.adsabs.harvard.edu/abs/2020A%26A...640A. 131M/abstract>`_ that was used for HR 8799 e. It requires ``tint``, ``alpa``, ``log_delta``, ``t1``, ``t2``, and ``t3`` as parameters. - Arbitrary number of free temperature nodes requires parameters ``t0``, ``t1``, ``t2``, etc. So counting from zero up to the number of nodes that are required. The nodes will be interpolated to a larger number of points in log-pressure space (set with the ``pressure_grid`` parameter) by using a cubic spline. Optionally, the ``pt_smooth`` parameter can also be included in ``model_param``, which is used for smoothing the interpolated P-T profile with a Gaussian kernel in :math:`\\log{P/\\mathrm{bar}}`. A recommended value for the kernel is 0.3 dex, so ``pt_smooth=0.3``. - Instead of a parametrization, it is also possible to provide a manual P-T profile as ``numpy`` array with the argument of ``pt_manual``. Cloud models (optional -- one of the options can be used): - Physical clouds as in `Mollière et al (2020) <https://ui.adsabs.harvard.edu/abs/2020A%26A...640A. 131M/abstract>`_ require the parameters ``fsed``, ``log_kzz``, and ``sigma_lnorm``. Cloud abundances are either specified relative to the equilibrium abundances (when using chemical equilibrium abundances for the line species) or as free abundances (when using free abundances for the line species). For the first case, the relative mass fractions are specified for example with the ``mgsio3_fraction`` parameter if the list with ``cloud_species`` contains ``MgSiO3(c)_cd``. - With the physical clouds, instead of including the mass fraction with the ``_fraction`` parameters, it is also possible to enforce the clouds (to ensure an effect on the spectrum) by scaling the opacities with the ``log_tau_cloud`` parameter. This is the wavelength-averaged optical depth of the clouds down to the gas-only photosphere. The abundances are now specified relative to the first cloud species that is listed in ``cloud_species``. The ratio parameters should be provided with the ``_ratio`` suffix. For example, if ``cloud_species=['MgSiO3(c)_cd', 'Fe(c)_cd', 'Al2O3(c)_cd']`` then the ``fe_mgsio3_ratio`` and ``al2o3_mgsio3_ratio`` parameters are required. - Instead of a single sedimentation parameter, ``fsed``, it is also possible to include two values, ``fsed_1`` and ``fsed_2``. This will calculate a weighted combination of two cloudy spectra, to mimic horizontal cloud variations. The weight should be provided with the ``f_clouds`` parameter (between 0 and 1) in the ``model_param`` dictionary. - Parametrized cloud opacities with a cloud absorption opacity, ``log_kappa_abs``, and powerlaw index, ``opa_abs_index``. Furthermore, ``log_p_base`` and ``fsed`` are required parameters. In addition to absorption, parametrized scattering opacities are added with the optional ``log_kappa_sca`` and ``opa_sca_index`` parameters. Optionally, the ``lambda_ray`` can be included, which is the wavelength at which the opacity changes to a :math:`\\lambda^{-4}` dependence in the Rayleigh regime. It is also possible to include ``log_tau_cloud``, which can be used for enforcing clouds in the photospheric region by scaling the cloud opacities. - Parametrized cloud opacities with a total cloud opacity, ``log_kappa_0``, and a single scattering albedo, ``albedo``. Furthermore, ``opa_index``, ``log_p_base``, and ``fsed``, are required parameters. This is `cloud model 2` from `Mollière et al (2020) <https://ui.adsabs.harvard. edu/abs/2020A%26A...640A.131M/abstract>`_ Optionally, ``log_tau_cloud`` can be used for enforcing clouds in the photospheric region by scaling the cloud opacities. - Gray clouds are simply parametrized with the ``log_kappa_gray`` and ``log_cloud_top`` parameters. These clouds extend from the bottom of the atmosphere up to the cloud top pressure and have a constant opacity. Optionally, a single scattering albedo, ``albedo``, can be specified. Also ``log_tau_cloud`` can be used for enforcing clouds in the photospheric region by scaling the cloud opacities. Extinction (optional): - Extinction can optionally be applied to the spectrum by including the ``ism_ext`` parameter, which is the the visual extinction, $A_V$. The empirical relation from `Cardelli et al. (1989) <https://ui.adsabs. harvard.edu/abs/1989ApJ...345..245C/abstract>`_ is used for calculating the extinction at other wavelengths. - When using ``ism_ext``, the reddening, $R_V$, can also be optionaly set with the ``ism_red`` parameter. Otherwise it is set to the standard value for the diffuse ISM, $R_V = 3.1$. Radial velocity and broadening (optional): - Radial velocity shift can be applied by adding the ``rad_vel`` parameter. This shifts the spectrum by a constant velocity (km/s). - Rotational broadening can be applied by adding the ``vsini`` parameter, which is the projected spin velocity (km/s), :math:`v\\sin{i}`. The broadening is applied with the ``fastRotBroad`` function from ``PyAstronomy`` (see for details the `documentation <https://pyastronomy.readthedocs.io/en/latest/ pyaslDoc/aslDoc/ rotBroad.html#fastrotbroad-a- faster-algorithm>`_). quenching : str, None Quenching type for CO/CH$_4$/H$_2$O abundances. Either the quenching pressure (bar) is a free parameter (``quenching='pressure'``) or the quenching pressure is calculated from the mixing and chemical timescales (``quenching='diffusion'``). The quenching is not applied if the argument is set to ``None``. spec_res : float, None Spectral resolution, achieved by smoothing with a Gaussian kernel. No smoothing is applied when the argument is set to ``None``. wavel_resample : np.ndarray, None Wavelength points (:math:`\\mu`m) to which the spectrum will be resampled. The original wavelengths points will be used if the argument is set to ``None``. plot_contribution : bool, str, None Filename for the plot with the emission contribution. The plot is not created if the argument is set to ``False`` or ``None``. If set to ``True``, the plot is shown in an interface window instead of written to a file. temp_nodes : int, None Number of free temperature nodes. Returns ------- species.core.box.ModelBox Box with the petitRADTRANS model spectrum. """ # Set chemistry type if "metallicity" in model_param and "c_o_ratio" in model_param: chemistry = "equilibrium" else: chemistry = "free" check_nodes = {} for line_item in self.line_species: abund_count = 0 for node_idx in range(100): if f"{line_item}_{node_idx}" in model_param: abund_count += 1 else: break check_nodes[line_item] = abund_count # Check if there are an equal number of # abundance nodes for all the line species nodes_list = list(check_nodes.values()) if not all(value == nodes_list[0] for value in nodes_list): raise ValueError( "The number of abundance nodes is " "not equal for all the lines " f"species: {check_nodes}" ) if all(value == 0 for value in nodes_list): abund_nodes = None else: abund_nodes = nodes_list[0] for line_item in self.line_species: if abund_nodes is None: if line_item not in model_param: raise RuntimeError( f"The abundance of {line_item} is not " "found in the dictionary with parameters " "of 'model_param'. Please add the log10 " f"mass fraction of {line_item}." ) else: for node_idx in range(abund_nodes): if f"{line_item}_{node_idx}" not in model_param: raise RuntimeError( f"The abundance of {line_item} is not " "found in the dictionary with parameters " "of 'model_param'. Please add the log10 " f"mass fraction of {line_item}." ) # Check quenching parameter if not hasattr(self, "quenching"): self.quenching = quenching if self.quenching is not None and chemistry != "equilibrium": raise ValueError( "The 'quenching' parameter can only be used in combination with " "chemistry='equilibrium'." ) if self.quenching is not None and self.quenching not in [ "pressure", "diffusion", ]: raise ValueError( "The argument of 'quenching' should be of the following: " "'pressure', 'diffusion', or None." ) # Abundance nodes if chemistry == "free" and abund_nodes is not None: knot_press_abund = np.logspace( np.log10(self.pressure[0]), np.log10(self.pressure[-1]), abund_nodes ) else: knot_press_abund = None # C/O and [Fe/H] if chemistry == "equilibrium": # Equilibrium chemistry metallicity = model_param["metallicity"] c_o_ratio = model_param["c_o_ratio"] log_x_abund = None elif chemistry == "free": # Free chemistry # TODO Set [Fe/H] = 0 for Molliere P-T profile and # cloud condensation profiles metallicity = 0.0 # Get smoothing parameter for abundance profiles if "abund_smooth" in model_param: self.abund_smooth = model_param["abund_smooth"] # Create a dictionary with the mass fractions if abund_nodes is None: log_x_abund = {} for line_item in self.line_species: log_x_abund[line_item] = model_param[line_item] _, _, c_o_ratio = calc_metal_ratio(log_x_abund, self.line_species) else: log_x_abund = {} for line_item in self.line_species: for node_idx in range(abund_nodes): log_x_abund[f"{line_item}_{node_idx}"] = model_param[ f"{line_item}_{node_idx}" ] # TODO Set C/O = 0.55 for Molliere P-T profile # and cloud condensation profiles c_o_ratio = 0.55 # Create the P-T profile if self.pressure_grid == "manual": temp = self.pt_manual[:, 1] elif ( "tint" in model_param and "log_delta" in model_param and "alpha" in model_param ): temp, _, _ = pt_ret_model( np.array([model_param["t1"], model_param["t2"], model_param["t3"]]), 10.0 ** model_param["log_delta"], model_param["alpha"], model_param["tint"], self.pressure, metallicity, c_o_ratio, ) elif "tint" in model_param and "log_delta" in model_param: tau = self.pressure * 1e6 * 10.0 ** model_param["log_delta"] temp = (0.75 * model_param["tint"] ** 4.0 * (2.0 / 3.0 + tau)) ** 0.25 elif "PTslope_1" in model_param: num_layer = 6 # could make a variable in the future layer_pt_slopes = np.ones(num_layer) * np.nan for index in range(num_layer): layer_pt_slopes[index] = model_param[f"PTslope_{num_layer - index}"] try: from petitRADTRANS.physics import dTdP_temperature_profile except ImportError as import_error: raise ImportError( "Can't import the dTdP profile function from " "petitRADTRANS, check that your version of pRT " "includes this function in petitRADTRANS.physics", import_error, ) temp = dTdP_temperature_profile( self.pressure, num_layer, # could change in the future layer_pt_slopes, model_param["T_bottom"], ) else: if temp_nodes is None: temp_nodes = 0 for temp_idx in range(100): if f"t{temp_idx}" in model_param: temp_nodes += 1 else: break knot_press = np.logspace( np.log10(self.pressure[0]), np.log10(self.pressure[-1]), temp_nodes ) knot_temp = [] for temp_idx in range(temp_nodes): knot_temp.append(model_param[f"t{temp_idx}"]) knot_temp = np.asarray(knot_temp) if "pt_smooth" in model_param: pt_smooth = model_param["pt_smooth"] else: pt_smooth = None temp = pt_spline_interp( knot_press, knot_temp, self.pressure, pt_smooth=pt_smooth, ) # Set the log quenching pressure, log(P/bar) if self.quenching == "pressure": p_quench = 10.0 ** model_param["log_p_quench"] elif self.quenching == "diffusion": p_quench = quench_pressure( self.pressure, temp, model_param["metallicity"], model_param["c_o_ratio"], model_param["logg"], model_param["log_kzz"], ) else: if "log_p_quench" in model_param: warnings.warn( "The 'model_param' dictionary contains the " "'log_p_quench' parameter but 'quenching=None'. " "The quenching pressure from the dictionary is " "therefore ignored." ) p_quench = None if ( len(self.cloud_species) > 0 or "log_kappa_0" in model_param or "log_kappa_gray" in model_param or "log_kappa_abs" in model_param ): tau_cloud = None log_x_base = None if ( "log_kappa_0" in model_param or "log_kappa_gray" in model_param or "log_kappa_abs" in model_param ): if "log_tau_cloud" in model_param: tau_cloud = 10.0 ** model_param["log_tau_cloud"] elif "tau_cloud" in model_param: tau_cloud = model_param["tau_cloud"] elif chemistry == "equilibrium": # Create the dictionary with the mass fractions of the # clouds relative to the maximum values allowed from # elemental abundances cloud_fractions = {} for item in self.cloud_species: if f"{item[:-3].lower()}_fraction" in model_param: cloud_fractions[item] = model_param[ f"{item[:-3].lower()}_fraction" ] elif f"{item[:-3].lower()}_tau" in model_param: # Import the chemistry module here because it is slow from poor_mans_nonequ_chem.poor_mans_nonequ_chem import ( interpol_abundances, ) # Interpolate the abundances, following chemical equilibrium abund_in = interpol_abundances( np.full(self.pressure.size, c_o_ratio), np.full(self.pressure.size, metallicity), temp, self.pressure, Pquench_carbon=p_quench, ) # Extract the mean molecular weight mmw = abund_in["MMW"] # Calculate the scaled mass fraction of the clouds cloud_fractions[item] = scale_cloud_abund( model_param, self.rt_object, self.pressure, temp, mmw, "equilibrium", abund_in, item, model_param[f"{item[:-3].lower()}_tau"], pressure_grid=self.pressure_grid, ) if "log_tau_cloud" in model_param: # Set the log mass fraction to zero and use the # optical depth parameter to scale the cloud mass # fraction with petitRADTRANS tau_cloud = 10.0 ** model_param["log_tau_cloud"] elif "tau_cloud" in model_param: # Set the log mass fraction to zero and use the # optical depth parameter to scale the cloud mass # fraction with petitRADTRANS tau_cloud = model_param["tau_cloud"] if tau_cloud is not None: for i, item in enumerate(self.cloud_species): if i == 0: cloud_fractions[item] = 0.0 else: cloud_1 = item[:-3].lower() cloud_2 = self.cloud_species[0][:-3].lower() cloud_fractions[item] = model_param[ f"{cloud_1}_{cloud_2}_ratio" ] # Create a dictionary with the log mass fractions at the cloud base log_x_base = log_x_cloud_base(c_o_ratio, metallicity, cloud_fractions) elif chemistry == "free": # Add the log10 mass fractions of the clouds to the dictionary log_x_base = {} if "log_tau_cloud" in model_param: # Set the log mass fraction to zero and use the # optical depth parameter to scale the cloud mass # fraction with petitRADTRANS tau_cloud = 10.0 ** model_param["log_tau_cloud"] elif "tau_cloud" in model_param: # Set the log mass fraction to zero and use the # optical depth parameter to scale the cloud mass # fraction with petitRADTRANS tau_cloud = model_param["tau_cloud"] if tau_cloud is None: for item in self.cloud_species: # Set the log10 of the mass fractions at th # cloud base equal to the value from the # parameter dictionary log_x_base[item[:-3]] = model_param[item] else: # Set the log10 of the mass fractions with the # ratios from the parameter dictionary and # scale to the actual mass fractions with # tau_cloud that is used in calc_spectrum_clouds for i, item in enumerate(self.cloud_species): if i == 0: log_x_base[item[:-3]] = 0.0 else: cloud_1 = item[:-3].lower() cloud_2 = self.cloud_species[0][:-3].lower() log_x_base[item[:-3]] = model_param[ f"{cloud_1}_{cloud_2}_ratio" ] # Calculate the petitRADTRANS spectrum # for a cloudy atmosphere if "fsed_1" in model_param and "fsed_2" in model_param: cloud_dict = model_param.copy() cloud_dict["fsed"] = cloud_dict["fsed_1"] ( wavelength, flux_1, emission_contr_1, _, ) = calc_spectrum_clouds( self.rt_object, self.pressure, temp, c_o_ratio, metallicity, p_quench, log_x_abund, log_x_base, cloud_dict, model_param["logg"], chemistry=chemistry, knot_press_abund=knot_press_abund, abund_smooth=self.abund_smooth, pressure_grid=self.pressure_grid, plotting=False, contribution=True, tau_cloud=tau_cloud, cloud_wavel=self.cloud_wavel, ) cloud_dict = model_param.copy() cloud_dict["fsed"] = cloud_dict["fsed_2"] ( wavelength, flux_2, emission_contr_2, _, ) = calc_spectrum_clouds( self.rt_object, self.pressure, temp, c_o_ratio, metallicity, p_quench, log_x_abund, log_x_base, cloud_dict, model_param["logg"], chemistry=chemistry, knot_press_abund=knot_press_abund, abund_smooth=self.abund_smooth, pressure_grid=self.pressure_grid, plotting=False, contribution=True, tau_cloud=tau_cloud, cloud_wavel=self.cloud_wavel, ) flux = ( model_param["f_clouds"] * flux_1 + (1.0 - model_param["f_clouds"]) * flux_2 ) emission_contr = ( model_param["f_clouds"] * emission_contr_1 + (1.0 - model_param["f_clouds"]) * emission_contr_2 ) else: ( wavelength, flux, emission_contr, _, ) = calc_spectrum_clouds( self.rt_object, self.pressure, temp, c_o_ratio, metallicity, p_quench, log_x_abund, log_x_base, model_param, model_param["logg"], chemistry=chemistry, knot_press_abund=knot_press_abund, abund_smooth=self.abund_smooth, pressure_grid=self.pressure_grid, plotting=False, contribution=True, tau_cloud=tau_cloud, cloud_wavel=self.cloud_wavel, ) elif chemistry == "equilibrium": # Calculate the petitRADTRANS spectrum for a clear atmosphere wavelength, flux, emission_contr = calc_spectrum_clear( self.rt_object, self.pressure, temp, model_param["logg"], model_param["c_o_ratio"], model_param["metallicity"], p_quench, None, pressure_grid=self.pressure_grid, chemistry=chemistry, knot_press_abund=knot_press_abund, abund_smooth=self.abund_smooth, contribution=True, ) elif chemistry == "free": log_x_abund = {} if abund_nodes is None: for line_item in self.rt_object.line_species: log_x_abund[line_item] = model_param[line_item] else: for line_item in self.rt_object.line_species: for node_idx in range(abund_nodes): log_x_abund[f"{line_item}_{node_idx}"] = model_param[ f"{line_item}_{node_idx}" ] wavelength, flux, emission_contr = calc_spectrum_clear( self.rt_object, self.pressure, temp, model_param["logg"], None, None, None, log_x_abund, chemistry=chemistry, knot_press_abund=knot_press_abund, abund_smooth=self.abund_smooth, pressure_grid=self.pressure_grid, contribution=True, ) if "radius" in model_param: # Calculate the planet mass from log(g) and radius model_param["mass"] = logg_to_mass( model_param["logg"], model_param["radius"] ) # Scale the flux to the observer if "parallax" in model_param: scaling = (model_param["radius"] * constants.R_JUP) ** 2 / ( 1e3 * constants.PARSEC / model_param["parallax"] ) ** 2 flux *= scaling elif "distance" in model_param: scaling = (model_param["radius"] * constants.R_JUP) ** 2 / ( model_param["distance"] * constants.PARSEC ) ** 2 flux *= scaling # Apply ISM extinction if "ism_ext" in model_param: if "ism_red" in model_param: ism_reddening = model_param["ism_red"] else: # Use default ISM reddening (R_V = 3.1) if ism_red is not provided ism_reddening = 3.1 flux = apply_ism_ext( wavelength, flux, model_param["ism_ext"], ism_reddening ) # Plot 2D emission contribution if plot_contribution: # Calculate the total optical depth (line and continuum opacities) # self.rt_object.calc_opt_depth(10.**model_param['logg']) # From Paul: The first axis of total_tau is the coordinate # of the cumulative opacity distribution function (ranging # from 0 to 1). A correct average is obtained by # multiplying the first axis with self.w_gauss, then # summing them. This is then the actual wavelength-mean. if self.scattering: # From petitRADTRANS: Only use 0 index for species # because for lbl or test_ck_shuffle_comp = True # everything has been moved into the 0th index w_gauss = self.rt_object.w_gauss[..., np.newaxis, np.newaxis] optical_depth = np.sum( w_gauss * self.rt_object.total_tau[:, :, 0, :], axis=0 ) else: # TODO Is this correct? w_gauss = self.rt_object.w_gauss[ ..., np.newaxis, np.newaxis, np.newaxis ] optical_depth = np.sum( w_gauss * self.rt_object.total_tau[:, :, :, :], axis=0 ) # Sum over all species optical_depth = np.sum(optical_depth, axis=1) plt.rcParams["font.family"] = "serif" plt.rcParams["mathtext.fontset"] = "dejavuserif" plt.figure(figsize=(8.0, 4.0)) gridsp = mpl.gridspec.GridSpec(1, 1) gridsp.update(wspace=0, hspace=0, left=0, right=1, bottom=0, top=1) ax = plt.subplot(gridsp[0, 0]) ax.tick_params( axis="both", which="major", colors="black", labelcolor="black", direction="in", width=1, length=5, labelsize=12, top=True, bottom=True, left=True, right=True, ) ax.tick_params( axis="both", which="minor", colors="black", labelcolor="black", direction="in", width=1, length=3, labelsize=12, top=True, bottom=True, left=True, right=True, ) ax.set_xlabel("Wavelength (µm)", fontsize=13) ax.set_ylabel("Pressure (bar)", fontsize=13) ax.get_xaxis().set_label_coords(0.5, -0.09) ax.get_yaxis().set_label_coords(-0.07, 0.5) ax.set_yscale("log") ax.xaxis.set_major_locator(MultipleLocator(1.0)) ax.xaxis.set_minor_locator(MultipleLocator(0.2)) press_bar = 1e-6 * self.rt_object.press # (Ba) -> (Bar) xx_grid, yy_grid = np.meshgrid(wavelength, press_bar) emission_contr = np.nan_to_num(emission_contr) ax.pcolormesh( xx_grid, yy_grid, emission_contr, cmap=plt.cm.bone_r, shading="gouraud", ) photo_press = np.zeros(wavelength.shape[0]) for i in range(photo_press.shape[0]): press_interp = interp1d(optical_depth[i, :], self.rt_object.press) photo_press[i] = press_interp(1.0) * 1e-6 # cgs to (bar) ax.plot(wavelength, photo_press, lw=0.5, color="gray") ax.set_xlim(np.amin(wavelength), np.amax(wavelength)) ax.set_ylim(np.amax(press_bar), np.amin(press_bar)) if isinstance(plot_contribution, str): plt.savefig(plot_contribution, bbox_inches="tight") else: plt.show() plt.clf() plt.close() # Convolve with a broadening kernel for vsin(i) if "vsini" in model_param: # fastRotBroad requires a regular # wavelength sampling while pRT uses # a logarithmic wavelength sampling wavel_even = np.linspace( np.amin(wavelength), np.amax(wavelength), wavelength.size * 10 ) # So change temporarily to a linear sampling # with a factor 10 larger number of wavelengths spec_interp = interp1d(wavelength, flux) flux_even = spec_interp(wavel_even) # Apply the rotational broadening spec_broad = fastRotBroad( wvl=wavel_even, flux=flux_even, epsilon=1.0, vsini=model_param["vsini"], effWvl=None, ) # The rotBroad function is much slower than # fastRotBroad when tested on a large array # spec_broad = rotBroad(wvl=wavel_even, # flux=flux_even, # epsilon=1., # vsini=model_param['vsini'], # edgeHandling='firstlast') # And change back to the original (logarithmic) # wavelength sampling, with constant R spec_interp = interp1d(wavel_even, spec_broad) flux = spec_interp(wavelength) # Convolve the spectrum with a Gaussian LSF if spec_res is not None: flux = convolve_spectrum(wavelength, flux, spec_res) # Apply a radial velocity shift to the wavelengths if "rad_vel" in model_param: # Change speed of light from (m/s) to (km/s) wavelength *= 1.0 - model_param["rad_vel"] / (constants.LIGHT * 1e-3) # Resample the spectrum if wavel_resample is not None: flux = spectres.spectres( wavel_resample, wavelength, flux, spec_errs=None, fill=np.nan, verbose=True, ) wavelength = wavel_resample if hasattr(self.rt_object, "h_bol"): pressure = 1e-6 * self.rt_object.press # (bar) f_bol = -4.0 * np.pi * self.rt_object.h_bol f_bol *= 1e-3 # (erg s-1 cm-2) -> (W m-2) bol_flux = np.column_stack((pressure, f_bol)) else: bol_flux = None return create_box( boxtype="model", model="petitradtrans", wavelength=wavelength, flux=flux, parameters=model_param, quantity="flux", contribution=emission_contr, bol_flux=bol_flux, )
[docs] @typechecked def get_flux(self, model_param: Dict[str, float]) -> Tuple[float, None]: """ Function for calculating the filter-weighted flux density for the ``filter_name``. Parameters ---------- model_param : dict Dictionary with the model parameters and values. Returns ------- float Flux (W m-2 um-1). NoneType Uncertainty (W m-2 um-1). Always set to ``None``. """ if "log_p_quench" in model_param: quenching = "pressure" else: quenching = None spectrum = self.get_model(model_param, quenching=quenching) synphot = SyntheticPhotometry(self.filter_name) return synphot.spectrum_to_flux(spectrum.wavelength, spectrum.flux)
[docs] @typechecked def get_magnitude(self, model_param: Dict[str, float]) -> Tuple[float, None]: """ Function for calculating the magnitude for the ``filter_name``. Parameters ---------- model_param : dict Dictionary with the model parameters and values. Returns ------- float Magnitude. NoneType Uncertainty. Always set to ``None``. """ if "log_p_quench" in model_param: quenching = "pressure" else: quenching = None spectrum = self.get_model(model_param, quenching=quenching) synphot = SyntheticPhotometry(self.filter_name) app_mag, _ = synphot.spectrum_to_magnitude(spectrum.wavelength, spectrum.flux) return app_mag