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!")
../_images/examples_plotting_3_1.png

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')
../_images/examples_plotting_5_0.png

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')
../_images/examples_plotting_7_0.png

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>
../_images/examples_plotting_22_1.png
[ ]: