Basic Usage#

Setup#

This tutorial notebook will walk you through the basic usage of the OTTER API. First make sure you followed the installation structions to install otter and the data. First the imports we will need for the notebook

[1]:
import os
import otter

from astropy.coordinates import SkyCoord
from astropy import units as u

import matplotlib.pyplot as plt
import numpy as np

We can initiate the otter database object and then get metadata of all of the data. From there, we can easily get the default redshift for each object and histogram them.

Note: When calling get_meta it does not return the photometry. This is to make metadata queries much quicker. See below for also accessing the photometry along with metadata.

[2]:
db = otter.Otter()
meta = db.get_meta(classification="TDE")
redshifts = np.array([t.get_redshift() for t in meta if "distance" in t and len(t["distance"]) > 0])
fig, ax = plt.subplots()
ax.hist(redshifts.astype(float), bins=20)
ax.set_ylabel("N")
ax.set_xlabel("z")
Attempting to login to https://otter.idies.jhu.edu/api with the following credentials:
username: user-guest
password: test
[2]:
Text(0.5, 0, 'z')
../_images/examples_basic_usage_3_2.png

Querying the OTTER Catalog#

Now that we have the catalog read in an initiated, we can query it for the data we want. Say we want to get all of the data associated with the TDE Sw J1644+57, we can use

[3]:
swj1644_list = db.query(names='Sw J1644+57')
swj1644_list
[3]:
[Transient(
        Name: Sw J1644+57,
        Keys: dict_keys(['_key', '_id', '_rev', 'classification', 'name', 'coordinate', 'filter_alias', 'photometry', 'reference_alias', 'distance', 'host', 'date_reference', 'schema_version', '_dec', '_ra'])
 )]

Which you can see is a list of otter Transient objects. You can essentially treat this like a python dictionary with some benefits:

  • It has additional helpful methods to access and clean the data

  • You can use the HDF5 slash-syntax. Ex.) swj1644['name/alias'] will give you all of the aliases from the alias subkey in the name key

[4]:
swj1644 = swj1644_list[0] # just to get rid of the list

swj1644['name/alias']
[4]:
[{'value': 'GRB 110328A', 'reference': ['2011Sci...333..203B']},
 {'value': 'Swift J1644+57',
  'reference': ['2017ApJ...838..149A',
   '2011Sci...333..203B',
   '2023PASP..135c4101G']},
 {'value': 'Sw J1644+57', 'reference': ['Swift', '2011Natur.476..425Z']},
 {'value': 'Swift J164449.3+573451', 'reference': ['2011Sci...333..203B']},
 {'value': 'SwJ1644+57',
  'reference': ['2011Natur.476..425Z', '2011Natur.476..425Z']}]

So this TDE has multiple aliases associated with it! Let’s try to query by a different one

[5]:
db.query(names='GRB 110328A')[0]
[5]:
Transient(
        Name: Sw J1644+57,
        Keys: dict_keys(['_key', '_id', '_rev', 'classification', 'name', 'coordinate', 'filter_alias', 'photometry', 'reference_alias', 'distance', 'host', 'date_reference', 'schema_version', '_dec', '_ra'])
)

And look at that, it gives us the same answer! We can also query by a list of names which will give us a list of transient objects back:

[6]:
db.query(names=['Sw J1644+57', '2018hyz', '2018zr'])
[6]:
[Transient(
        Name: Sw J1644+57,
        Keys: dict_keys(['_key', '_id', '_rev', 'classification', 'name', 'coordinate', 'filter_alias', 'photometry', 'reference_alias', 'distance', 'host', 'date_reference', 'schema_version', '_dec', '_ra'])
 ),
 Transient(
        Name: 2018hyz,
        Keys: dict_keys(['_key', '_id', '_rev', '_dec', '_ra', 'classification', 'coordinate', 'date_reference', 'distance', 'filter_alias', 'name', 'photometry', 'reference_alias', 'schema_version'])
 ),
 Transient(
        Name: 2018zr,
        Keys: dict_keys(['_key', '_id', '_rev', 'reference_alias', 'classification', 'coordinate', 'distance', 'name', 'date_reference', 'host', 'schema_version', 'filter_alias', 'photometry', '_dec', '_ra'])
 )]

We can also search OTTER for anything within a certain redshift range, let’s look for everything between \(0.1 < z <0.2\):

[7]:
len(db.query(minz=0.1, maxz=0.2))
[7]:
1542

WOW! That’s a lot of transients. But that’s expected since many of them are discovered nearby. What if we look for everything with a redshift over 1?

[8]:
len(db.query(minz=1))
[8]:
63

Ah makes sense, it is just those rarer high redshift events.

Cone Searches#

Moving on from general querys, we can perform cone searches on the catalog using the otter.Otter.cone_search method. Let’s do this for objects within 15 degrees of the ecliptic pole.

[9]:
# first construct a SkyCoord
coord = SkyCoord('18h00m00.0s', '+66d33m38.84s', unit=(u.hourangle, u.deg))
radius = (15*u.deg).to(u.arcsec).value # must just provide it as a float

len(db.cone_search(coord, radius=radius))
[9]:
352

Sample Selection#

Given that OTTER includes many transients that are only classified based on photometric information, we provide numerous flags for selecting a subsamble of events based on your scientific goals:

  1. spec_classed is True if the event is spectroscopically classified

  2. unambiguous is True if the community unambiguously agrees on this classification

  3. class_confidence_threshold let’s you set a minimum for the “confidence” flag C

As an example, to select all of the events that are spectroscopically classified as TDEs you would do

[10]:
spec_classed_tdes = db.query(classification="TDE", spec_classed=True)

len(spec_classed_tdes)
[10]:
139

Or, to get all events that unambiguously spectrocsopically classified as TDEs:

[11]:
gold_spec_classed = db.query(classification="TDE", spec_classed=True, unambiguous=True)
len(gold_spec_classed)
[11]:
130

We can then see the TDE candidates that have classification spectra but their true nature is still up for debate:

[12]:
set(t.default_name for t in spec_classed_tdes) - set(t.default_name for t in gold_spec_classed)
[12]:
{'2017bcc',
 '2018dyk',
 '2018gn',
 'ASASSN-15lh',
 'CSS100217',
 'PTF09axc',
 'PTF09djl',
 'SN2015L',
 'SN2016ezh'}

Photometry#

Finally, we can also get the cleaned up photometry associated with the transients. Let’s use ['Sw J1644+57', '2018hyz', 'ASASSN-14li'] and get all of the associated photometry. This is not the fastest method but is worth the wait because you get out an astropy Table that you can then filter yourself!

[13]:
phot = db.get_phot(names=['Sw J1644+57', '2018hyz', 'ASASSN-14li'])
phot
Sw J1644+57 has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
/home/nfranz/research/astro-otter/otter/src/otter/io/transient.py:1072: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for val_av, grp in subset[outdata.corr_av == True].groupby("val_av"):
ASASSN-14li has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
Unable to apply the source mapping because '2021ApJ...908....4V'
/home/nfranz/research/astro-otter/otter/src/otter/io/transient.py:1072: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for val_av, grp in subset[outdata.corr_av == True].groupby("val_av"):
2018hyz has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
[13]:
Table length=2738
nameconverted_fluxconverted_flux_errconverted_dateconverted_waveconverted_freqconverted_flux_unitconverted_date_unitconverted_wave_unitconverted_freq_unitfilter_nameobs_typeupperlimitreferencehuman_readable_refstelescope
str11float64float64float64float64float64str7str3str2str3str9str5boolobjectobjectstr11
Sw J1644+5715.3256192999939780.1506524075713576455651.3119467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5715.0032285736670820.05597444424957901555652.1419467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.8112906329627170.0596966505591048955653.1219467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.6374801016666570.0653988665465764455654.0519467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.3380588438111330.0703184669117942255655.0619467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.3283502586616970.0655929284651122855656.2819467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.2123468415207470.0540339968489269455657.0619467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.1395640792895880.0815379259863228155659.1419467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
Sw J1644+5714.085431063438140.0349621461332429355660.0719467042.72727272715.399999999999999mag(AB)MJDnmGHzKuradioFalse2012ApJ...748...36BBerger et al. (2012)AMI-LA
................................................
2018hyz16.9262945339228850.0855187334214287458521.729894343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz16.964093526026670.0659385128746506958525.409411343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.1607505834491750.0745104815617320458536.997743343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.439466574718820.0986792121873025158541.047834343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.239685594361410.074575528599892758545.665269343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.395948652733950.080822960507376658549.845566343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.8905390866411120.144937729027679958579.159513343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz18.0676662422712120.1232767912671472258581.629376343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz17.802333693661630.128168786315628458584.276434343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--
2018hyz18.363500877670840.1975703765312474558666.12249343.4775240910411872815.3575502597mag(AB)MJDnmGHzUuvoirFalse['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']['2020MNRAS.498.4119S', '2021ApJ...908....4V', '2024MNRAS.527.2452M']--

We can also filter it more when we call the get_phot method by only requesting a specific observatory type from the options of radio, uvoir, or xray. So, say we just want all the radio data associated with these three objects. And, since radio data is rarely reported as AB magnitudes, lets get the flux out as a flux density in units of microjanskies. We can also request that it returns a pandas DataFrame instead of an astropy Table using the return_type='pandas' keyword.

[14]:
radiophot = db.get_phot(names=['Sw J1644+57', '2018hyz', 'ASASSN-14li'], obs_type='radio',
                        flux_unit='uJy', return_type='pandas')
radiophot
Sw J1644+57 has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
/home/nfranz/research/astro-otter/otter/src/otter/io/transient.py:1072: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for val_av, grp in subset[outdata.corr_av == True].groupby("val_av"):
ASASSN-14li has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
/home/nfranz/research/astro-otter/otter/src/otter/io/transient.py:1072: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
  for val_av, grp in subset[outdata.corr_av == True].groupby("val_av"):
2018hyz has at least one photometry point where it is unclear if a host subtraction was performed. This can be especially detrimental for UV data. Please consider filtering out UV/Optical/IR or radio rows where the corr_host column is null/None/NaN.
[14]:
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
0 Sw J1644+57 2690.0 440.0 55651.31 1.946704e+07 15.4 uJy MJD nm GHz Ku radio False 2012ApJ...748...36B Berger et al. (2012) AMI-LA
1 Sw J1644+57 3620.0 220.0 55652.14 1.946704e+07 15.4 uJy MJD nm GHz Ku radio False 2012ApJ...748...36B Berger et al. (2012) AMI-LA
2 Sw J1644+57 4320.0 280.0 55653.12 1.946704e+07 15.4 uJy MJD nm GHz Ku radio False 2012ApJ...748...36B Berger et al. (2012) AMI-LA
3 Sw J1644+57 5070.0 360.0 55654.05 1.946704e+07 15.4 uJy MJD nm GHz Ku radio False 2012ApJ...748...36B Berger et al. (2012) AMI-LA
4 Sw J1644+57 6680.0 510.0 55655.06 1.946704e+07 15.4 uJy MJD nm GHz Ku radio False 2012ApJ...748...36B Berger et al. (2012) AMI-LA
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
220 2018hyz 3449.0 41.0 59603.00 4.282749e+07 7.0 uJy MJD nm GHz C radio False 2026ApJ...998..111C Cendes, Yvette et al. (2026) VLA
221 2018hyz 4553.0 140.0 59655.00 4.282749e+07 7.0 uJy MJD nm GHz C radio False 2026ApJ...998..111C Cendes, Yvette et al. (2026) VLA
222 2018hyz 1870.0 32.0 59530.00 3.331027e+07 9.0 uJy MJD nm GHz X radio False 2026ApJ...998..111C Cendes, Yvette et al. (2026) VLA
223 2018hyz 3056.0 54.0 59603.00 3.331027e+07 9.0 uJy MJD nm GHz X radio False 2026ApJ...998..111C Cendes, Yvette et al. (2026) VLA
224 2018hyz 4249.0 124.0 59655.00 3.331027e+07 9.0 uJy MJD nm GHz X radio False 2026ApJ...998..111C Cendes, Yvette et al. (2026) VLA

888 rows × 16 columns

Then we can do things like plot the light curve of all of these values!

[15]:
fig, axs = plt.subplots(1,3, figsize=(18,6))

for (name, data), ax in zip(radiophot.groupby('name'), axs):

    discovery_date = db.get_meta(names=name)[0].get_discovery_date().mjd

    for (f, df) in data.groupby('filter_name'):
        fig = otter.plotter.plotter.plot_light_curve(date=df.converted_date - discovery_date,
                                             flux=df.converted_flux,
                                             flux_err=df.converted_flux_err,
                                             ax=ax,
                                             fig=fig,
                                             marker='o',
                                             linestyle='none',
                                             label=f'{name}: {f}')

    ax.legend()
    ax.set_xlim(10,4000)
    ax.set_xscale("log")
    ax.set_yscale("log")
../_images/examples_basic_usage_30_0.png

Where the colors represent the observation frequency! Obviously these are not pubblication ready but they are at least a good way to easily visualize the data while working with it.