"""
Module with functions for plotting the results obtained with the
:class:`~species.fit.fit_evolution.FitEvolution` class.
"""
from numbers import Real
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from beartype import beartype
from beartype.typing import List, Optional, Tuple
from matplotlib.ticker import AutoMinorLocator
from species.read.read_isochrone import ReadIsochrone
from species.util.core_util import print_section
[docs]
@beartype
def plot_cooling(
tag: str,
n_samples: int = 50,
cooling_param: str = "log_lum",
xlim: Optional[Tuple[Real, Real]] = None,
ylim: Optional[Tuple[Real, Real]] = None,
xscale: Optional[str] = "linear",
yscale: Optional[str] = "linear",
title: Optional[str] = None,
offset: Optional[Tuple[Real, Real]] = None,
figsize: Optional[Tuple[Real, Real]] = (4.0, 2.5),
output: Optional[str] = None,
) -> Tuple[mpl.figure.Figure, List[List[List[np.ndarray]]], np.ndarray]:
"""
Function for plotting samples of cooling tracks that are
randomly drawn from the posterior distributions of the
age and mass parameters that have been estimated with
:class:`~species.fit.fit_evolution.FitEvolution`.
Parameters
----------
tag : str
Database tag where the samples are stored
n_samples : int
Number of randomly drawn cooling tracks that will be plotted.
cooling_param : str
Type of cooling parameter that will be plotted
('log_lum', 'radius', 'teff', or 'logg').
xlim : tuple(float, float), None
Limits of the wavelength axis. Automatic limits are used if
the argument is set to ``None``.
ylim : tuple(float, float), None
Limits of the flux axis. Automatic limits are used if
the argument is set to ``None``.
xscale : str, None
Scale of the x axis ('linear' or 'log'). The scale is set
to ``'linear'`` if the argument is set to ``None``.
yscale : str, None
Scale of the x axis ('linear' or 'log'). The scale is set
to ``'linear'`` if the argument is set to ``None``.
title : str
Title to show at the top of the plot.
offset : tuple(float, float)
Offset for the label of the x- and y-axis.
figsize : tuple(float, float)
Figure size.
output : str, None
Output filename for the plot. The plot is shown in an
interface window if the argument is set to ``None``.
Returns
-------
matplotlib.figure.Figure
The ``Figure`` object that can be used for further
customization of the plot.
np.ndarray
Array with the cooling tracks. The array contains
:math:`L/L_\\odot` or radius as function of time
for each companion and sample, so the shape is
(n_companions, n_samples, n_ages).
np.ndarray
Array with the random indices that have been
sampled from the posterior distribution.
"""
print_section("Plot cooling tracks")
print(f"Database tag: {tag}")
print(f"Number of samples: {n_samples}")
print(f"Model parameters: {cooling_param}")
plt.close()
if cooling_param not in ["log_lum", "luminosity", "radius", "teff", "logg"]:
raise ValueError(
"The argument of 'cooling_parameter' is "
"not valid and should be set to "
"'log_lum', 'radius', 'teff', or 'logg'."
)
from species.data.database import Database
species_db = Database()
samples_box = species_db.get_samples(tag)
samples = samples_box.samples
attr = samples_box.attributes
n_param = attr["n_param"]
n_planets = attr["n_planets"]
model_name = attr["model_name"]
log_lum = attr["log_lum"]
age_prior = attr["age_prior"]
radius_prior = attr["radius_prior"]
param_indices = {}
for i in range(n_param):
param_indices[samples_box.attributes[f"parameter{i}"]] = i
if np.isnan(age_prior[0]):
param_idx = samples_box.parameters.index("age")
age_prior = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
else:
# Asymmetric normal prior set in FitEvolution
age_prior = [
age_prior[0],
age_prior[0] + age_prior[1],
age_prior[0] + age_prior[2],
]
read_iso = ReadIsochrone(model_name)
plt.rcParams["font.family"] = "serif"
plt.rcParams["mathtext.fontset"] = "dejavuserif"
fig = plt.figure(1, figsize=figsize)
gridsp = mpl.gridspec.GridSpec(n_planets, 1)
gridsp.update(wspace=0, hspace=0.1, left=0, right=1, bottom=0, top=1)
ax = []
for i in range(n_planets):
ax.append(plt.subplot(gridsp[i, 0]))
if xscale is None:
xscale = "linear"
if yscale is None:
yscale = "linear"
cool_tracks = []
for i in range(n_planets):
cool_tracks.append([])
for i in range(n_planets):
if not isinstance(radius_prior[i], np.ndarray) and np.isnan(radius_prior[i]):
param_idx = samples_box.parameters.index(f"radius_{i}")
radius_tmp = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
else:
radius_tmp = [
radius_prior[i][0],
radius_prior[i][0] - radius_prior[i][1],
radius_prior[i][0] + radius_prior[i][1],
]
param_idx = samples_box.parameters.index(f"teff_{i}")
teff_tmp = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
param_idx = samples_box.parameters.index(f"logg_{i}")
logg_tmp = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
ax[i].set_xscale(xscale)
ax[i].set_yscale(yscale)
labelbottom = bool(i == n_planets - 1)
ax[i].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,
labelbottom=labelbottom,
)
ax[i].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,
labelbottom=labelbottom,
)
if xscale == "linear":
ax[i].xaxis.set_minor_locator(AutoMinorLocator(5))
if i == n_planets - 1:
ax[i].set_xlabel("Age (Myr)", fontsize=13)
if cooling_param in ["luminosity", "log_lum"]:
ax[i].set_ylabel(r"$\log(L/L_\odot)$", fontsize=13)
elif cooling_param == "radius":
ax[i].set_ylabel(r"Radius ($R_\mathrm{J}$)", fontsize=13)
elif cooling_param == "teff":
ax[i].set_ylabel(r"$T_\mathrm{eff}$ (K)", fontsize=13)
elif cooling_param == "logg":
ax[i].set_ylabel(r"$\log\,g$", fontsize=13)
if xlim is not None:
ax[i].set_xlim(xlim[0], xlim[1])
if ylim is not None:
ax[i].set_ylim(ylim[0], ylim[1])
if offset is not None:
ax[i].get_xaxis().set_label_coords(0.5, offset[0])
ax[i].get_yaxis().set_label_coords(offset[1], 0.5)
ran_indices = np.random.randint(low=0, high=samples.shape[0], size=n_samples)
for sample_idx in ran_indices:
for planet_idx in range(n_planets):
mass = samples[sample_idx, param_indices[f"mass_{planet_idx}"]]
if f"s_init_{planet_idx}" in param_indices:
s_init = samples[sample_idx, param_indices[f"s_init_{planet_idx}"]]
else:
s_init = None
cool_box = read_iso.get_cooling_track(
mass=mass, ages=None, s_init=s_init
)
if cooling_param in ["luminosity", "log_lum"]:
cool_tracks[planet_idx].append([cool_box.age, cool_box.log_lum])
elif cooling_param == "radius":
cool_tracks[planet_idx].append([cool_box.age, cool_box.radius])
elif cooling_param == "teff":
cool_tracks[planet_idx].append([cool_box.age, cool_box.teff])
elif cooling_param == "logg":
cool_tracks[planet_idx].append([cool_box.age, cool_box.logg])
if cool_tracks[planet_idx][-1][1] is None:
raise ValueError(
f"The selected parameter, '{cooling_param}', "
f"is not part of the '{model_name}' "
"evolutionary model grid."
)
ax[planet_idx].plot(
cool_tracks[planet_idx][-1][0],
cool_tracks[planet_idx][-1][1],
lw=0.5,
color="gray",
alpha=0.5,
)
for i in range(n_planets):
if cooling_param in ["luminosity", "log_lum"]:
ax[i].errorbar(
[age_prior[0]],
[log_lum[i][0]],
xerr=[
[age_prior[0] - np.abs(age_prior[1])],
[age_prior[2] - age_prior[0]],
],
yerr=[log_lum[i][1]],
color="tab:orange",
)
elif cooling_param == "radius":
ax[i].errorbar(
[age_prior[0]],
[radius_tmp[0]],
xerr=[
[age_prior[0] - np.abs(age_prior[1])],
[age_prior[2] - age_prior[0]],
],
yerr=[[radius_tmp[0] - radius_tmp[1]], [radius_tmp[2] - radius_tmp[0]]],
color="tab:orange",
)
elif cooling_param == "teff":
ax[i].errorbar(
[age_prior[0]],
[teff_tmp[0]],
xerr=[
[age_prior[0] - np.abs(age_prior[1])],
[age_prior[2] - age_prior[0]],
],
yerr=[[teff_tmp[0] - teff_tmp[1]], [teff_tmp[2] - teff_tmp[0]]],
color="tab:orange",
)
elif cooling_param == "logg":
ax[i].errorbar(
[age_prior[0]],
[logg_tmp[0]],
xerr=[
[age_prior[0] - np.abs(age_prior[1])],
[age_prior[2] - age_prior[0]],
],
yerr=[[logg_tmp[0] - logg_tmp[1]], [logg_tmp[2] - logg_tmp[0]]],
color="tab:orange",
)
if title is not None:
ax[0].set_title(title, fontsize=18.0)
if output is None:
plt.show()
else:
print(f"\nOutput: {output}")
plt.savefig(output, bbox_inches="tight")
return fig, cool_tracks, ran_indices
[docs]
@beartype
def plot_isochrones(
tag: str,
n_samples: int = 50,
xlim: Optional[Tuple[Real, Real]] = None,
ylim: Optional[Tuple[Real, Real]] = None,
xscale: Optional[str] = "linear",
yscale: Optional[str] = "linear",
title: Optional[str] = None,
offset: Optional[Tuple[Real, Real]] = None,
figsize: Optional[Tuple[Real, Real]] = (4.0, 2.5),
output: Optional[str] = None,
) -> Tuple[mpl.figure.Figure, List[List[List[np.ndarray]]], np.ndarray]:
"""
Function for plotting samples of isochrones that are
randomly drawn from the posterior distributions of the
age and mass parameters that have been estimated with
:class:`~species.fit.fit_evolution.FitEvolution`.
For each isochrone, the parameters from a single
posterior sample are used.
Parameters
----------
tag : str
Database tag where the samples are stored
n_samples : int
Number of randomly drawn cooling tracks that will be plotted.
xlim : tuple(float, float), None
Limits of the wavelength axis. Automatic limits are used if
the argument is set to ``None``.
ylim : tuple(float, float), None
Limits of the flux axis. Automatic limits are used if
the argument is set to ``None``.
xscale : str, None
Scale of the x axis ('linear' or 'log'). The scale is set
to ``'linear'`` if the argument is set to ``None``.
yscale : str, None
Scale of the x axis ('linear' or 'log'). The scale is set
to ``'linear'`` if the argument is set to ``None``.
title : str
Title to show at the top of the plot.
offset : tuple(float, float)
Offset for the label of the x- and y-axis.
figsize : tuple(float, float)
Figure size.
output : str, None
Output filename for the plot. The plot is shown in an
interface window if the argument is set to ``None``.
Returns
-------
matplotlib.figure.Figure
The ``Figure`` object that can be used for further
customization of the plot.
np.ndarray
Array with the isochrones. The array contains
:math:`L/L_\\odot` as function of time for each companions
and sample, so the shape is (n_companions, n_samples, n_masses).
np.ndarray
Array with the random indices that have been
sampled from the posterior distribution.
"""
print_section("Plot isochrones")
print(f"Database tag: {tag}")
print(f"Number of samples: {n_samples}")
plt.close()
from species.data.database import Database
species_db = Database()
samples_box = species_db.get_samples(tag)
samples = samples_box.samples
attr = samples_box.attributes
n_param = samples_box.attributes["n_param"]
n_planets = attr["n_planets"]
model_name = attr["model_name"]
log_lum = attr["log_lum"]
age_prior = attr["age_prior"]
mass_prior = attr["mass_prior"]
param_indices = {}
for i in range(n_param):
param_indices[samples_box.attributes[f"parameter{i}"]] = i
if np.isnan(age_prior[0]):
param_idx = samples_box.parameters.index("age")
age_prior = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
else:
# Asymmetric normal prior set in FitEvolution
age_prior = [
age_prior[0],
age_prior[0] + age_prior[1],
age_prior[0] + age_prior[2],
]
read_iso = ReadIsochrone(model_name)
plt.rcParams["font.family"] = "serif"
plt.rcParams["mathtext.fontset"] = "dejavuserif"
fig = plt.figure(1, figsize=figsize)
gridsp = mpl.gridspec.GridSpec(n_planets, 1)
gridsp.update(wspace=0, hspace=0.1, left=0, right=1, bottom=0, top=1)
ax = []
for i in range(n_planets):
ax.append(plt.subplot(gridsp[i, 0]))
if xscale is None:
xscale = "linear"
if yscale is None:
yscale = "linear"
isochrones = []
for i in range(n_planets):
isochrones.append([])
for i in range(n_planets):
labelbottom = bool(i == n_planets - 1)
ax[i].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,
labelbottom=labelbottom,
)
ax[i].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,
labelbottom=labelbottom,
)
if xscale == "linear":
ax[i].xaxis.set_minor_locator(AutoMinorLocator(5))
if i == n_planets - 1:
ax[i].set_xlabel(r"Mass ($M_\mathrm{J}$)", fontsize=13)
ax[i].set_ylabel(r"$\log(L/L_\odot)$", fontsize=13)
ax[i].set_xscale(xscale)
ax[i].set_yscale(yscale)
if xlim is not None:
ax[i].set_xlim(xlim[0], xlim[1])
if ylim is not None:
ax[i].set_ylim(ylim[0], ylim[1])
if offset is not None:
ax[i].get_xaxis().set_label_coords(0.5, offset[0])
ax[i].get_yaxis().set_label_coords(offset[1], 0.5)
ran_indices = np.random.randint(low=0, high=samples.shape[0], size=n_samples)
for sample_idx in ran_indices:
for planet_idx in range(n_planets):
age = samples[sample_idx, param_indices["age"]]
if f"s_init_{planet_idx}" in param_indices:
s_init = samples[sample_idx, param_indices[f"s_init_{planet_idx}"]]
else:
s_init = None
iso_box = read_iso.get_isochrone(
age=age, masses=None, s_init=s_init, param_interp=["log_lum"]
)
isochrones[planet_idx].append([iso_box.mass, iso_box.log_lum])
ax[planet_idx].plot(
isochrones[planet_idx][-1][0],
isochrones[planet_idx][-1][1],
lw=0.5,
color="gray",
alpha=0.5,
)
for i in range(n_planets):
if not isinstance(mass_prior[i], np.ndarray) and np.isnan(mass_prior[i]):
param_idx = samples_box.parameters.index(f"mass_{i}")
mass_tmp = np.percentile(samples[:, param_idx], [50.0, 16.0, 84.0])
else:
mass_tmp = [
mass_prior[i][0],
mass_prior[i][0] - mass_prior[i][1],
mass_prior[i][0] + mass_prior[i][1],
]
ax[i].errorbar(
[mass_tmp[0]],
[log_lum[i][0]],
xerr=[[mass_tmp[0] - mass_tmp[1]], [mass_tmp[2] - mass_tmp[0]]],
yerr=[log_lum[i][1]],
color="tab:orange",
)
if title is not None:
ax[0].set_title(title, fontsize=18.0)
if output is None:
print(f"\nOutput: {output}")
plt.show()
else:
plt.savefig(output, bbox_inches="tight")
return fig, isochrones, ran_indices