306.1. Transient light curves#
306.1. Transient light curves¶
For the Rubin Science Platform at data.lsst.cloud.
Data Release: Data Preview 1
Container Size: large
LSST Science Pipelines version: r29.2.0
Last verified to run: 2025-09-02
Repository: github.com/lsst/tutorial-notebooks
Learning objective: An overview of plotting extragalactic transient light curves in DP1.
LSST data products: ForcedSourceOnDiaObject
, DiaObject
, Visit
Packages: matplotlib
, numpy
, lsst.rsp
, lsst.utils.plotting
Credit: Originally developed by the Rubin Community Science team with feedback from Eric Bellm. This notebook also highlights a transient detection from LSSTComCam first identified by Dan Taranu. Please consider acknowledging them if this notebook is used for the preparation of journal articles, software releases, or other notebooks.
Get Support: Everyone is encouraged to ask questions or raise issues in the Support Category of the Rubin Community Forum. Rubin staff will respond to all questions posted there.
1. Introduction¶
This notebook demonstrates how to obtain and plot a difference-imaged light curve of an extragalactic transient captured in Data Preview 1 (DP1).
Related tutorials: There are 200-level tutorials on difference_images
as well as the DiaSource
, DiaObject
and ForcedSourceOnDiaObject
catalogs. There is another notebook in this 306 series (306.2) that demonstrates candidate transient identification.
1.1. Import packages¶
Import numpy
, a fundamental package for scientific computing with arrays in Python
(numpy.org), and
matplotlib
, a comprehensive library for data visualization
(matplotlib.org;
matplotlib gallery).
From the lsst
package, import modules for accessing the Table Access Protocol (TAP) service,
the butler, and image display functions from the LSST Science Pipelines (pipelines.lsst.io).
import matplotlib.pyplot as plt
import numpy as np
from lsst.rsp import get_tap_service
from lsst.utils.plotting import (get_multiband_plot_colors,
get_multiband_plot_symbols,)
1.2. Define parameters and functions¶
Create an instance of the TAP service, and assert that it exists.
service = get_tap_service("tap")
assert service is not None
Define filter names, plot markers, and colors for plotting
filter_names = ['u', 'g', 'r', 'i', 'z', 'y']
filter_colors = get_multiband_plot_colors()
filter_symbols = get_multiband_plot_symbols()
2. Retrieve light curve data¶
Obtain light curve data from the ForcedSourceOnDiaObject
table, which contains forced PSF photometry for all difference images as well as non-difference images (i.e. visit_image
). The relevant table columns for plotting light curves are:
psfDiffFlux
: Forced PSF photometry on difference images at DiaObject positiondiaObjectId
: Unique DiaObject identifierband
: Filter associated with flux measurementvisit
: Identifier of the visit where the forced photometry was measured (used for table JOIN onVisit
table)
The date information expMidptMJD
, which is mid-point time for the visit in MJD, can be obtained from the Visit
table.
The units used to plot the light curve are in flux (nJy) and not magnitudes since the negative flux measurements would be omitted when converting to magnituides. More information of the light curve can therefore be plotted in flux units.
The difference-image forced PSF photometry should be used to plot the extragalactic transient light curve instead of the non-difference-image forced PSF photometry because of potential contamination from the host galaxy in the non-difference image.
2.1. Look up diaObjectId from coordinates¶
The first step to obtain light curve data is to define the coordinates (ra and dec) of an extragalactic transient captured in DP1.
ra = 53.125
dec = -27.740
Define a 1.0 arcsecond search radius, and convert it to degrees.
search_rad = 1.0/3600
Search at this position in the DiaObject
table to retrieve the diaObjectId
of the corresponding object.
query = "SELECT ra, dec, diaObjectId "\
"FROM dp1.DiaObject "\
"WHERE CONTAINS(POINT('ICRS', ra, dec), "\
"CIRCLE('ICRS'," + str(ra) + ", "\
+ str(dec) + ", "+ str(search_rad) + ")) = 1 "
print(query)
SELECT ra, dec, diaObjectId FROM dp1.DiaObject WHERE CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS',53.125, -27.74, 0.0002777777777777778)) = 1
Run the TAP query.
job = service.submit_job(query)
job.run()
job.wait(phases=['COMPLETED', 'ERROR'])
print('Job phase is', job.phase)
if job.phase == 'ERROR':
job.raise_if_error()
Job phase is COMPLETED
Fetch the results of the TAP search in table form and print the unique entries for diaObjectId
in the output table.
assert job.phase == 'COMPLETED'
DiaObj = job.fetch_result().to_table()
print(np.unique(DiaObj['diaObjectId']))
diaObjectId ------------------ 611255759837069401
2.2. Search based on diaObjectId¶
Provide this diaObjectId
as a constraint to a TAP query to obtain the time series from ForcedSourceOnDiaObject
. The use of an ID in this manner is always the recommended way to access data from a ForcedSource table.
The diaObjectId
of the transient explored in this notebook is 611255759837069401.
DiaObjID = DiaObj['diaObjectId'][0]
query = "SELECT fsodo.coord_ra, fsodo.coord_dec, "\
"fsodo.diaObjectId, fsodo.visit, fsodo.band, "\
"fsodo.psfDiffFlux, fsodo.psfDiffFluxErr, "\
"fsodo.psfFlux as psfFlux, fsodo.psfFluxErr, "\
"vis.expMidptMJD "\
"FROM dp1.ForcedSourceOnDiaObject as fsodo "\
"JOIN dp1.Visit as vis ON vis.visit = fsodo.visit "\
"WHERE fsodo.diaObjectId = "+str(DiaObjID)
print(query)
SELECT fsodo.coord_ra, fsodo.coord_dec, fsodo.diaObjectId, fsodo.visit, fsodo.band, fsodo.psfDiffFlux, fsodo.psfDiffFluxErr, fsodo.psfFlux as psfFlux, fsodo.psfFluxErr, vis.expMidptMJD FROM dp1.ForcedSourceOnDiaObject as fsodo JOIN dp1.Visit as vis ON vis.visit = fsodo.visit WHERE fsodo.diaObjectId = 611255759837069401
Run the TAP query.
job = service.submit_job(query)
job.run()
job.wait(phases=['COMPLETED', 'ERROR'])
print('Job phase is', job.phase)
if job.phase == 'ERROR':
job.raise_if_error()
Job phase is COMPLETED
Fetch the results of the TAP search in table form and print the number of rows.
assert job.phase == 'COMPLETED'
forced_source = job.fetch_result().to_table()
print(len(forced_source))
446
Uncomment the following to view the table.
# forced_source
3. Plot light curve¶
Plot the forced PSF photometry on the difference images covering the extragalactic transient.
fig, ax = plt.subplots(1, 1, figsize=(10, 6), sharey=True, sharex=False)
for f, filt in enumerate(filter_names):
fx = np.where(forced_source['band'] == filt)[0]
ax.errorbar(
forced_source['expMidptMJD'][fx],
forced_source['psfDiffFlux'][fx],
yerr=forced_source['psfDiffFluxErr'][fx],
fmt=filter_symbols[filt],
ms=10,
mew=2,
mec=filter_colors[filt],
ecolor=filter_colors[filt],
alpha=0.5,
color='none',
label=filt
)
del fx
ax.set_xlabel('Modified Julian Date')
ax.set_ylabel('Difference-Image Flux (nJy)')
ax.set_title('Forced PSF Photometry (ForcedSourceOnDiaObject)')
ax.set_ylim(-13000, 15000)
ax.legend(loc='upper right')
plt.subplots_adjust(wspace=.0)
plt.show()
Figure 1: Forced PSF (difference-image) photometry of an extragalactic transient in DP1 from the
ForcedSourceOnDiaObject
table. Note that the difference-image flux is negative because the reference template images used to perform the image subtraction likely captured emission from the transient. This is not surprising given that the temporal baseline of DP1 may not have been long enough to build a reference template that did not include emission from the transient.