"""
Module with reading functionalities for Planck spectra.
"""
import os
import warnings
from configparser import ConfigParser
from typing import Dict, List, Optional, Tuple, Union
import numpy as np
import spectres
from typeguard import typechecked
from species.core import constants
from species.core.box import ColorMagBox, ColorColorBox, ModelBox, create_box
from species.phot.syn_phot import SyntheticPhotometry
from species.read.read_filter import ReadFilter
from species.util.spec_util import create_wavelengths, smooth_spectrum
[docs]
class ReadPlanck:
"""
Class for reading a Planck spectrum.
"""
@typechecked
def __init__(
self,
wavel_range: Optional[
Tuple[Union[float, np.float32], Union[float, np.float32]]
] = None,
filter_name: Optional[str] = None,
) -> None:
"""
Parameters
----------
wavel_range : tuple(float, float), None
Wavelength range (um). A wavelength range of 0.1-1000 um
is used if set to ``None``. Not used if ``filter_name``
is not set to ``None``.
filter_name : str, None
Filter name that is used for the wavelength range. The
``wavel_range`` is used if set to ``None``.
Returns
-------
NoneType
None
"""
self.spectrum_interp = None
self.wl_points = None
self.wl_index = None
self.filter_name = filter_name
self.wavel_range = wavel_range
if self.filter_name is not None:
transmission = ReadFilter(self.filter_name)
self.wavel_range = transmission.wavelength_range()
elif self.wavel_range is None:
self.wavel_range = (0.1, 1000.0)
config_file = os.path.join(os.getcwd(), "species_config.ini")
config = ConfigParser()
config.read(config_file)
self.database = config["species"]["database"]
[docs]
@staticmethod
@typechecked
def planck(
wavel_points: np.ndarray, temperature: float, scaling: float
) -> np.ndarray:
"""
Internal function for calculating a Planck function.
Parameters
----------
wavel_points : np.ndarray
Wavelength points (um).
temperature : float
Temperature (K).
scaling : float
Scaling parameter.
Returns
-------
np.ndarray
Flux density (W m-2 um-1).
"""
planck_1 = (
2.0 * constants.PLANCK * constants.LIGHT**2 / (1e-6 * wavel_points) ** 5
)
planck_2 = (
np.exp(
constants.PLANCK
* constants.LIGHT
/ (1e-6 * wavel_points * constants.BOLTZMANN * temperature)
)
- 1.0
)
return 1e-6 * np.pi * scaling * planck_1 / planck_2 # (W m-2 um-1)
[docs]
@staticmethod
@typechecked
def update_parameters(
model_param: Dict[str, Union[float, List[float]]]
) -> Dict[str, float]:
"""
Internal function for updating the dictionary with model
parameters.
Parameters
----------
model_param : dict
Dictionary with the 'teff' (K), 'radius' (Rjup), and
'parallax' (mas) or 'distance' (pc). The values of
'teff' and 'radius' can be a single float, or a list
with floats for a combination of multiple Planck
functions, e.g. {'teff': [1500., 1000.],
'radius': [1., 2.], 'distance': 10.}.
Returns
-------
dict
Updated dictionary with model parameters.
"""
updated_param = {}
for i, _ in enumerate(model_param["teff"]):
updated_param[f"teff_{i}"] = model_param["teff"][i]
updated_param[f"radius_{i}"] = model_param["radius"][i]
if "parallax" in model_param:
updated_param["parallax"] = model_param["parallax"]
elif "distance" in model_param:
updated_param["distance"] = model_param["distance"]
return updated_param
[docs]
@typechecked
def get_spectrum(
self,
model_param: Dict[str, Union[float, List[float]]],
spec_res: Optional[float] = None,
wavel_resample: Optional[np.ndarray] = None,
**kwargs,
) -> ModelBox:
"""
Function for calculating a Planck spectrum or a combination of
multiple Planck spectra. The spectrum is calculated at
:math:`R = 500`. Afterwards, an optional smoothing and
wavelength resampling can be applied.
Parameters
----------
model_param : dict
Dictionary with the 'teff' (K), 'radius' (Rjup), and
'parallax' (mas) or 'distance' (pc). The values of
'teff' and 'radius' can be a single float, or a list
with floats for a combination of multiple Planck
functions, e.g. {'teff': [1500., 1000.],
'radius': [1., 2.], 'distance': 10.}.
spec_res : float, None
Spectral resolution that is used for smoothing the
spectrum with a Gaussian kernel. No smoothing is
applied if the argument is set to ``None``.
wavel_resample : np.ndarray, None
Wavelength points (:math:`\\mu`m) to which the spectrum
will be resampled. The resampling is applied after the
optional smoothing to the value of ``spec_res``.
Returns
-------
species.core.box.ModelBox
Box with the Planck spectrum.
"""
if "smooth" in kwargs:
warnings.warn(
"The 'smooth' parameter has been "
"deprecated. Please set only the "
"'spec_res' argument, which can be set "
"to None for not applying a smoothing.",
DeprecationWarning,
)
if not kwargs["smooth"] and spec_res is not None:
spec_res = None
if "teff" in model_param and isinstance(model_param["teff"], list):
model_param = self.update_parameters(model_param)
wavel_points = create_wavelengths(self.wavel_range, 1000.0)
n_planck = 0
for item in model_param:
if item[:4] == "teff":
n_planck += 1
if n_planck == 1:
if "radius" in model_param and "parallax" in model_param:
scaling = (
(model_param["radius"] * constants.R_JUP)
/ (1e3 * constants.PARSEC / model_param["parallax"])
) ** 2
elif "radius" in model_param and "distance" in model_param:
scaling = (
(model_param["radius"] * constants.R_JUP)
/ (model_param["distance"] * constants.PARSEC)
) ** 2
else:
scaling = 1.0
flux = self.planck(
wavel_points, model_param["teff"], scaling
) # (W m-2 um-1)
else:
flux = np.zeros(wavel_points.shape)
for i in range(n_planck):
if f"radius_{i}" in model_param and "parallax" in model_param:
scaling = (
(model_param[f"radius_{i}"] * constants.R_JUP)
/ (1e3 * constants.PARSEC / model_param["parallax"])
) ** 2
elif f"radius_{i}" in model_param and "distance" in model_param:
scaling = (
(model_param[f"radius_{i}"] * constants.R_JUP)
/ (model_param["distance"] * constants.PARSEC)
) ** 2
else:
scaling = 1.0
flux += self.planck(
wavel_points, model_param[f"teff_{i}"], scaling
) # (W m-2 um-1)
if spec_res is not None:
flux = smooth_spectrum(wavel_points, flux, spec_res)
model_box = create_box(
boxtype="model",
model="planck",
wavelength=wavel_points,
flux=flux,
parameters=model_param,
quantity="flux",
)
if wavel_resample is not None:
flux = spectres.spectres(
wavel_resample,
wavel_points,
flux,
spec_errs=None,
fill=np.nan,
verbose=True,
)
model_box.wavelength = wavel_resample
model_box.flux = flux
if n_planck == 1 and "radius" in model_param:
model_box.parameters["luminosity"] = (
4.0
* np.pi
* (model_box.parameters["radius"] * constants.R_JUP) ** 2
* constants.SIGMA_SB
* model_box.parameters["teff"] ** 4.0
/ constants.L_SUN
) # (Lsun)
elif n_planck > 1:
lum_total = 0.0
for i in range(n_planck):
if f"radius_{i}" in model_box.parameters:
# Add up the luminosity of the blackbody components (Lsun)
surface = (
4.0
* np.pi
* (model_box.parameters[f"radius_{i}"] * constants.R_JUP) ** 2
)
lum_total += (
surface
* constants.SIGMA_SB
* model_box.parameters[f"teff_{i}"] ** 4.0
/ constants.L_SUN
)
if lum_total > 0.0:
model_box.parameters["luminosity"] = lum_total
return model_box
[docs]
@typechecked
def get_flux(
self, model_param: Dict[str, Union[float, List[float]]], synphot=None
) -> Tuple[float, None]:
"""
Function for calculating the average flux
density for the ``filter_name``.
Parameters
----------
model_param : dict
Dictionary with the 'teff' (K), 'radius' (Rjup), and
'parallax' (mas) or 'distance' (pc).
synphot : SyntheticPhotometry, None
Synthetic photometry object. The object is created if the
argument is set to ``None``.
Returns
-------
float
Average flux density (W m-2 um-1).
NoneType
None
"""
if "teff" in model_param and isinstance(model_param["teff"], list):
model_param = self.update_parameters(model_param)
spectrum = self.get_spectrum(model_param, 100.0)
if synphot is None:
synphot = SyntheticPhotometry(self.filter_name)
return synphot.spectrum_to_flux(spectrum.wavelength, spectrum.flux)
[docs]
@typechecked
def get_magnitude(
self, model_param: Dict[str, Union[float, List[float]]], synphot=None
) -> Tuple[Tuple[float, None], Tuple[float, None]]:
"""
Function for calculating the magnitude for the ``filter_name``.
Parameters
----------
model_param : dict
Dictionary with the 'teff' (K), 'radius' (Rjup), and
'parallax' (mas) or 'distance' (pc).
synphot : SyntheticPhotometry, None
Synthetic photometry object. The object is created if the
argument is set to ``None``.
Returns
-------
float
Apparent magnitude (mag).
float
Absolute magnitude (mag)
"""
if "teff" in model_param and isinstance(model_param["teff"], list):
model_param = self.update_parameters(model_param)
spectrum = self.get_spectrum(model_param, 100.0)
if synphot is None:
synphot = SyntheticPhotometry(self.filter_name)
if "parallax" in model_param:
distance = 1e3 / model_param["parallax"]
else:
distance = model_param["distance"]
return synphot.spectrum_to_magnitude(
spectrum.wavelength, spectrum.flux, distance=(distance, None)
)
[docs]
@staticmethod
@typechecked
def get_color_magnitude(
temperatures: np.ndarray,
radius: float,
filters_color: Tuple[str, str],
filter_mag: str,
) -> ColorMagBox:
"""
Function for calculating the colors and magnitudes in the range of 100-10000 K.
Parameters
----------
temperatures : np.ndarray
Temperatures (K) for which the colors and magnitude are
calculated.
radius : float
Radius (Rjup).
filters_color : tuple(str, str)
Filter names for the color.
filter_mag : str
Filter name for the absolute magnitudes.
Returns
-------
species.core.box.ColorMagBox
Box with the colors and magnitudes.
"""
list_color = []
list_mag = []
for item in temperatures:
model_param = {"teff": item, "radius": radius, "distance": 10.0}
read_planck_0 = ReadPlanck(filter_name=filters_color[0])
read_planck_1 = ReadPlanck(filter_name=filters_color[1])
read_planck_2 = ReadPlanck(filter_name=filter_mag)
app_mag_0, _ = read_planck_0.get_magnitude(model_param)
app_mag_1, _ = read_planck_1.get_magnitude(model_param)
app_mag_2, _ = read_planck_2.get_magnitude(model_param)
list_color.append(app_mag_0[0] - app_mag_1[0])
list_mag.append(app_mag_2[0])
return create_box(
boxtype="colormag",
library="planck",
object_type=None,
filters_color=filters_color,
filter_mag=filter_mag,
color=list_color,
magnitude=list_mag,
sptype=temperatures,
names=None,
)
[docs]
@staticmethod
@typechecked
def get_color_color(
temperatures: np.ndarray,
radius: float,
filters_colors: Tuple[Tuple[str, str], Tuple[str, str]],
) -> ColorColorBox:
"""
Function for calculating two colors in the range of
100-10000 K.
Parameters
----------
temperatures : np.ndarray
Temperatures (K) for which the colors are calculated.
radius : float
Radius (Rjup).
filters_colors : tuple(tuple(str, str), tuple(str, str))
Two tuples with the filter names for the colors.
Returns
-------
species.core.box.ColorColorBox
Box with the colors.
"""
list_color_1 = []
list_color_2 = []
for item in temperatures:
model_param = {"teff": item, "radius": radius, "distance": 10.0}
read_planck_0 = ReadPlanck(filter_name=filters_colors[0][0])
read_planck_1 = ReadPlanck(filter_name=filters_colors[0][1])
read_planck_2 = ReadPlanck(filter_name=filters_colors[1][0])
read_planck_3 = ReadPlanck(filter_name=filters_colors[1][1])
app_mag_0, _ = read_planck_0.get_magnitude(model_param)
app_mag_1, _ = read_planck_1.get_magnitude(model_param)
app_mag_2, _ = read_planck_2.get_magnitude(model_param)
app_mag_3, _ = read_planck_3.get_magnitude(model_param)
list_color_1.append(app_mag_0[0] - app_mag_1[0])
list_color_2.append(app_mag_2[0] - app_mag_3[0])
return create_box(
boxtype="colorcolor",
library="planck",
object_type=None,
filters=filters_colors,
color1=list_color_1,
color2=list_color_2,
sptype=temperatures,
names=None,
)