Quick Look Plotting#
There is some basic plotting code built into otter.plotter library for quickly viewing the SED and lightcurves of individual transients.
Like always, we import what we need and then load in the otter dataset:
[ ]:
%load_ext autoreload
%autoreload 2
import os
import otter
import numpy as np
import pandas as pd
import plotly
import matplotlib.pyplot as plt
db = otter.Otter()
Basic Plotting#
The easiest way to then plot the data is using the query_quick_view method which will query OTTER based on the typical keyword arguments you give it. For example, here we are querying for all transients with redshift greater than 1. As we saw in the basic_usage.ipynb tutorial, this should return three plots. But, there is no UV/Optical/IR data associated with two of them in OTTER so it only created one for AT2022cmc.
[2]:
figs = otter.query_quick_view(db, minz=1, plotting_kwargs=dict(linestyle='none', marker='o'), phot_cleaning_kwargs=dict(obs_type='uvoir'))
/home/nfranz/astro-otter/otter/src/otter/io/transient.py:632: UserWarning: Unable to apply the source mapping because Cannot set a DataFrame with multiple columns to the single column human_readable_refs
warnings.warn(f"Unable to apply the source mapping because {exc}")
/home/nfranz/astro-otter/otter/src/otter/plotter/plotter.py:80: UserWarning: No photometry associated with Sw J2058+05, skipping!
warn(f"No photometry associated with {t.default_name}, skipping!")
/home/nfranz/astro-otter/otter/src/otter/plotter/plotter.py:80: UserWarning: No photometry associated with CXOU J0332, skipping!
warn(f"No photometry associated with {t.default_name}, skipping!")
Instead of querying for objects, say we already have a Transient object that we want to look at the data for. For this we can instead use the quick_view method!
First, we plot the light curve:
[3]:
t = db.query(names='AT2018hyz')[0]
fig = otter.quick_view(t, ptype='lc', plotting_kwargs=dict(marker='o', linestyle='none'), flux_unit='mJy', obs_type='radio')
axs = fig.get_axes()
axs[0].set_yscale('log')
Then the spectral energy distribution:
[4]:
fig = otter.quick_view(t, ptype='sed', dt_over_t=0.0001, plotting_kwargs=dict(marker='o', linestyle='none'), flux_unit='mJy', obs_type='radio')
Note that you can also put these on a subplot together with the keyword ptype='both'!
Slightly More Complex Plotting#
This next part becomes very useful if you want to generate interactive figures. To do this, we switch the plotting backend from matplotlib to plotly and use the plot_light_curve and plot_sed methods directly.
Let’s keep using the AT2018hyz dataset since it is pretty complete. First we need to get the photometry associated with this object:
[5]:
phot = db.get_phot(names="AT2018hyz", return_type='pandas', obs_type='radio', flux_unit='mJy')
phot
[5]:
| name | converted_flux | converted_flux_err | converted_date | converted_wave | converted_freq | converted_flux_unit | converted_date_unit | converted_wave_unit | converted_freq_unit | filter_name | obs_type | upperlimit | reference | human_readable_refs | telescope | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 735 | 2018hyz | 0.038 | 0.000 | 58450.00 | 3.074794e+06 | 97.5 | mJy | MJD | nm | GHz | alma.3 | radio | True | 2020MNRAS.497.1925G | Gomez et al. (2020) | ALMA |
| 736 | 2018hyz | 0.043 | 0.000 | 58471.00 | 3.074794e+06 | 97.5 | mJy | MJD | nm | GHz | alma.3 | radio | True | 2020MNRAS.497.1925G | Gomez et al. (2020) | ALMA |
| 737 | 2018hyz | 0.451 | 0.029 | 59544.00 | 3.074794e+06 | 97.5 | mJy | MJD | nm | GHz | alma.3 | radio | False | 2022ApJ...938...28C | Cendes et al. (2022) | ALMA |
| 738 | 2018hyz | 0.769 | 0.023 | 59605.00 | 3.074794e+06 | 97.5 | mJy | MJD | nm | GHz | alma.3 | radio | False | 2022ApJ...938...28C | Cendes et al. (2022) | ALMA |
| 739 | 2018hyz | 1.264 | 0.018 | 59657.00 | 3.074794e+06 | 97.5 | mJy | MJD | nm | GHz | alma.3 | radio | False | 2022ApJ...938...28C | Cendes et al. (2022) | ALMA |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 23 | 2018hyz | 1.388 | NaN | 59400.64 | 5.995849e+07 | 5.0 | mJy | MJD | nm | GHz | C | radio | False | [2022ApJ...938...28C] | Cendes et al. (2022) | NaN |
| 24 | 2018hyz | 2.939 | NaN | 59554.64 | 5.995849e+07 | 5.0 | mJy | MJD | nm | GHz | C | radio | False | [2022ApJ...938...28C] | Cendes et al. (2022) | NaN |
| 25 | 2018hyz | 4.381 | NaN | 59627.64 | 5.995849e+07 | 5.0 | mJy | MJD | nm | GHz | C | radio | False | [2022ApJ...938...28C] | Cendes et al. (2022) | NaN |
| 26 | 2018hyz | 4.807 | NaN | 59679.64 | 5.995849e+07 | 5.0 | mJy | MJD | nm | GHz | C | radio | False | [2022ApJ...938...28C] | Cendes et al. (2022) | NaN |
| 31 | 2018hyz | 7.837 | NaN | 59724.64 | 5.450772e+07 | 5.5 | mJy | MJD | nm | GHz | C | radio | False | [2022ApJ...938...28C] | Cendes et al. (2022) | NaN |
128 rows × 16 columns
We can then generate a light curve using the plotly backend. For more details on the optional kwargs I pass in see https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html#plotly.graph_objects.Figure.add_scatter
[6]:
discovery_date = db.get_meta(names='AT2018hyz')[0].get_discovery_date().mjd
# split up the dataset by the filter name and then plot each independently
graph_object = plotly.graph_objects.Figure()
for filter_name, df in phot.groupby('filter_name'):
fig = otter.plot_light_curve(
date = df.converted_date - discovery_date,
flux = df.converted_flux,
flux_err = df.converted_flux_err,
backend = 'plotly',
ylabel = 'Flux [mJy]',
xlabel = 'MJD',
ax = graph_object, # pass in a graph object to the ax keyword
name = filter_name,
mode='markers'
)
fig.update_xaxes(type='log')
And we can do the same with SEDs
[7]:
# split up the dataset by the filter name and then plot each independently
graph_object = plotly.graph_objects.Figure()
for filter_name, df in phot.groupby('converted_date'):
fig = otter.plot_sed(
wave_or_freq = df.converted_freq,
flux = df.converted_flux,
flux_err = df.converted_flux_err,
backend = 'plotly',
ylabel = 'Flux [mJy]',
xlabel = 'Frequency [GHz]',
ax = graph_object, # pass in a graph object to the ax keyword
name = filter_name,
mode='markers'
)
fig.update_xaxes(type='log')
More Complex Plotting: Asymmetric Errorbars#
Because of the nature of the otter data storage, we store only one error for each flux value. However, this can be limiting when the raw data really has Asymmetric Errorbars. This section will walk through getting and plotting the data with these assymetric_errorbars.
We will use x-ray observations of the IR selected TDE WTP15acbgpn.
[8]:
t2 = db.query(names='WTP15acbgpn')[0]
phot = t2.clean_photometry(obs_type='xray', flux_unit='Jy')
phot.raw_err_detail
WARNING: UnitsWarning: 'erg/s/cm^2' contains multiple slashes, which is discouraged by the FITS standard [astropy.units.format.generic]
WARNING: UnitsWarning: 'erg/s/cm^2' contains multiple slashes, which is discouraged by the FITS standard [astropy.units.format.generic]
[8]:
90 {'upper': [9.999999999999999e-14, 5.6000000000...
91 {'upper': [9.999999999999999e-14, 5.6000000000...
92 {'upper': [2.41769675e-13], 'lower': [2.020235...
Name: raw_err_detail, dtype: object
As you can see, the raw_err_detail column holds dictionaries with the corresponding upper and lower values on the asymmetric errorbars. NOTE: These values will never be converted so we have to do that by hand!
To add them to the dataframe in a better format we can use some pandas groupby magic:
[9]:
cleaned_phot = []
# first, create a column of the dictionaries as a string
# this is necessary because python dictionaries are not hashable
phot['raw_err_detail_str'] = phot.raw_err_detail.astype(str)
# then we can groupby the string version of the dictionaries
for _, p in phot.groupby('raw_err_detail_str'):
err_detail_df = pd.DataFrame(p.raw_err_detail.iloc[0]) # convert the dict in the first row to a dataframe
err_detail_df = err_detail_df.set_index(p.index) # align the indices
df = pd.concat([p, err_detail_df], axis=1) # horizontally merge these dataframes
cleaned_phot.append(df)
# then combine all the data!
cleaned_phot = pd.concat(cleaned_phot)
cleaned_phot
[9]:
| date | date_format | filter_key | raw | raw_err | raw_units | telescope | upperlimit | corr_k | corr_s | ... | converted_freq | converted_freq_unit | converted_flux | converted_flux_err | converted_flux_unit | converted_date | converted_date_unit | raw_err_detail_str | upper | lower | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 92 | 58078.083919 | mjd | 0.3-10 | 7.897495e-13 | 2.218966e-13 | erg/s/cm^2 | Swift | False | False | False | ... | 2.345450e+09 | GHz | 5.606876e-07 | 1.575369e-07 | Jy | 58078.083919 | MJD | {'upper': [2.41769675e-13], 'lower': [2.020235... | 2.417697e-13 | 2.020235e-13 |
| 90 | 55424.364387 | mjd | 0.5-7 | 7.820000e-13 | 9.950000e-14 | erg/s/cm^2 | Chandra | False | False | False | ... | 1.571693e+09 | GHz | 3.465099e-07 | 4.408917e-08 | Jy | 55424.364387 | MJD | {'upper': [9.999999999999999e-14, 5.6000000000... | 1.000000e-13 | 9.900000e-14 |
| 91 | 55374.843319 | mjd | 0.5-7 | 7.430000e-13 | 5.550000e-14 | erg/s/cm^2 | Chandra | False | False | False | ... | 1.571693e+09 | GHz | 3.292287e-07 | 2.459245e-08 | Jy | 55374.843319 | MJD | {'upper': [9.999999999999999e-14, 5.6000000000... | 5.600000e-14 | 5.500000e-14 |
3 rows × 36 columns
So now we have upper and lower columns that describe the asymmetric errorbars for each flux value! Next, we need to convert these.
Since we have the raw values and converted flux values, this is pretty straight forward! Just using proportionalities we can use the following approximations to find the converted uncertainties.
[10]:
cleaned_phot['converted_upper'] = cleaned_phot.converted_flux * (cleaned_phot.upper / cleaned_phot.raw)
cleaned_phot['converted_lower'] = cleaned_phot.converted_flux * (cleaned_phot.lower / cleaned_phot.raw)
cleaned_phot
[10]:
| date | date_format | filter_key | raw | raw_err | raw_units | telescope | upperlimit | corr_k | corr_s | ... | converted_flux | converted_flux_err | converted_flux_unit | converted_date | converted_date_unit | raw_err_detail_str | upper | lower | converted_upper | converted_lower | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 92 | 58078.083919 | mjd | 0.3-10 | 7.897495e-13 | 2.218966e-13 | erg/s/cm^2 | Swift | False | False | False | ... | 5.606876e-07 | 1.575369e-07 | Jy | 58078.083919 | MJD | {'upper': [2.41769675e-13], 'lower': [2.020235... | 2.417697e-13 | 2.020235e-13 | 1.716459e-07 | 1.434279e-07 |
| 90 | 55424.364387 | mjd | 0.5-7 | 7.820000e-13 | 9.950000e-14 | erg/s/cm^2 | Chandra | False | False | False | ... | 3.465099e-07 | 4.408917e-08 | Jy | 55424.364387 | MJD | {'upper': [9.999999999999999e-14, 5.6000000000... | 1.000000e-13 | 9.900000e-14 | 4.431073e-08 | 4.386762e-08 |
| 91 | 55374.843319 | mjd | 0.5-7 | 7.430000e-13 | 5.550000e-14 | erg/s/cm^2 | Chandra | False | False | False | ... | 3.292287e-07 | 2.459245e-08 | Jy | 55374.843319 | MJD | {'upper': [9.999999999999999e-14, 5.6000000000... | 5.600000e-14 | 5.500000e-14 | 2.481401e-08 | 2.437090e-08 |
3 rows × 38 columns
Finally, we can plot this data similarly to above! The only difference is that we will use the matplotlib backend and pass in a list of tuples for the flux_err keyword.
[11]:
fig, ax = plt.subplots()
for filter_name, df in cleaned_phot.groupby('filter_name'):
otter.plot_light_curve(
date = df.converted_date,
flux = df.converted_flux,
flux_err = df[['converted_upper', 'converted_lower']].values.T,
fig=fig,
ax=ax,
linestyle='none',
marker='o',
label=f'{filter_name}keV'
)
ax.set_yscale('log')
ax.legend()
[11]:
<matplotlib.legend.Legend at 0x735204b2a250>
[ ]: