308.4. Photometry#
308.4. Solar System object photometry¶
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: 2026-06-03
Repository: github.com/lsst/tutorial-notebooks
DOI: 10.11578/rubin/dc.20250909.20
Learning objective: Guidance on photometry measures for Solar System objects in Data Preview 1.
LSST data products: DiaSource
Packages: lsst.rsp
Credit: Originally developed by the Rubin Community Science team. 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 provides an overview of the different types of Solar System Object (SSO) photometry measurements that are available in Data Preview 1.
Source detection.
In a series of visit images an SSO appears as a source moving relative to the fixed background sky.
When difference image analysis (DIA) is performed, an SSO appears as sources in the difference images.
Sources are considered "detected" if they have a signal-to-noise ratio SNR$>$5, and a detection in a difference image is referred to as a diaSource.
The Solar System Processing (SSP) pipeline links together detections of new and known solar system objects, and interfaces with the Minor Planet Center (MPC) for physical parameter derivation and orbit fitting.
Flux measurements.
A variety of flux measurements are made for each diaSource, and stored in the DiaSource table.
The following are the most relevant for SSOs.
apFlux,apFluxErr: Flux measured within a 12 pixel radius aperture, in nJy.psfFlux,psfFluxErr: A forced fit of the Point Spread Function (PSF) at thediaSourcecoordinates in each difference image, in nJy.trailFlux,trailLength: A PSF-fit that has been convolved with linear motion, in nJy, and the length of the trail in pixels.scienceFlux,scienceFluxErr: A forced fit of the Point Spread Function (PSF) at thediaSourcecoordinates in each visit image (also called the "direct" or "science" image), in nJy.
Note, for static-sky objects the ForcedSourceOnDiaObject table is recommended for transient and variable star photometry (see, e.g., tutorial 205.1).
Known issues with moving object photometry.
The trailed flux measurement has no error. This will be fixed in future data releases.
Moving objects sometimes appear in the template images, because DP1 data was obtained over a short timescale (2 to 21 distinct nights, depending on the field). This can significantly affect the
diaSourceflux measurements, and even lead to a dipole-like appearance in the difference image. FordiaSourcedetections flagged as a dipole, it may be better to use the science-image flux rather than the difference-image flux, as will be demonstrated in this notebook. However, this guidance only applies to DP1. In the future, when template images are not contaminated with moving objects, the difference-image flux is better to use because the science image will contain background astrophysical objects near thediaSource.
Related tutorials: The 102-series demonstrates the TAP service. The 103-series demonstrates the image display tools and image cutouts. The 201-series has notebooks on the DiaSource and SSObject tables, and the 202-series has notebooks on visit and difference images.
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).
Import fnmatch to help with string pattern matching.
From the lsst package, import modules for accessing the Table Access Protocol (TAP) service, the Butler, two-dimensional geometry, and displaying images.
import numpy as np
import fnmatch
from copy import deepcopy
import matplotlib.pyplot as plt
from lsst.rsp import get_tap_service
from lsst.daf.butler import Butler
from lsst.geom import Box2I, Point2I, Extent2I
import lsst.afw.display as afw_display
1.2. Define parameters and functions¶
Get an instance of the TAP service, and assert that it exists.
service = get_tap_service("tap")
assert service is not None
Get an instance of the Butler, and assert that it exists.
butler = Butler("dp1", collections="LSSTComCam/DP1")
assert butler is not None
Set the back-end for the image display tool afw_display to matplotlib.
afw_display.setDefaultBackend('matplotlib')
Load a colorblind-friendly palette and define the colors to use.
plt.style.use('seaborn-v0_8-colorblind')
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
2. DiaSource schema¶
The recommended way to explore table schema is to use the interactive Rubin schema browser.
Alternatively, use the TAP service to retrieve the schema for the DiaSource table, and store them as an AstroPy table using the to_table method, then clear the query and job.
query = "SELECT column_name, datatype, description, unit " \
"FROM tap_schema.columns " \
"WHERE table_name = 'dp1.DiaSource'"
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()
assert job.phase == 'COMPLETED'
results = job.fetch_result()
schema_DiaSource = results.to_table()
del query
job.delete()
Job phase is COMPLETED
Option to display the full schema of the DiaSource table.
# schema_DiaSource
2.1. Flux columns¶
Option to print the descriptions for all columns with "Flux" in their name.
# indices = [i for (i, col) in zip(range(len(schema_DiaSource)),
# schema_DiaSource['column_name'])
# if fnmatch.fnmatch(col, '*Flux*')]
# schema_DiaSource[indices]
Print the descriptions for the SSO-relevant flux columns described in Section 1.
flux_columns = ['apFlux', 'apFluxErr', 'psfFlux', 'psfFluxErr',
'trailFlux', 'trailLength', 'scienceFlux', 'scienceFluxErr']
schema_DiaSource[np.isin(schema_DiaSource['column_name'], flux_columns)]
| column_name | datatype | description | unit |
|---|---|---|---|
| str64 | str64 | str512 | str64 |
| apFlux | float | Flux in a 12 pixel radius aperture on the difference image. | nJy |
| apFluxErr | float | Estimated uncertainty of apFlux. | nJy |
| psfFlux | float | Flux derived from linear least-squares fit of PSF model. | nJy |
| psfFluxErr | float | Flux uncertainty derived from linear least-squares fit of PSF model. | nJy |
| trailFlux | float | Trailed source flux. | nJy |
| trailLength | double | Trail length. | pixel |
| scienceFlux | float | Forced PSF flux measured on the direct image. | nJy |
| scienceFluxErr | float | Forced PSF flux uncertainty measured on the direct image. | nJy |
2.2. Flag columns¶
There are a number of columns in the DiaSource table corresponding to flags which indicate when various situations have arisen during the processing, such as dipole sources, saturated sources, or cosmic ray effects.
Flags are helpful for assessing discrepancies or issues with photometric measurements.
Option to find and print all column names that contain the word "Flag" (or "flag").
# indices = [i for (i, col) in zip(range(len(schema_DiaSource)),
# schema_DiaSource['column_name']) \
# if fnmatch.fnmatch(col, '*flag*') or fnmatch.fnmatch(col, '*Flag*')]
# schema_DiaSource[indices]
Select and display all columns that have a data type of boolean, as these are mostly all flags.
schema_DiaSource_flags = schema_DiaSource[schema_DiaSource['datatype'] == 'boolean']
Option to display the list of boolean columns (there are many, and the table display will auto-truncate).
# schema_DiaSource_flags
Create an array list of the flag column names to use in the next section.
flag_columns = np.array(schema_DiaSource_flags['column_name'])
Delete from memory the tables that are no longer needed.
del schema_DiaSource, schema_DiaSource_flags
3. DiaSource photometry data¶
As the DP1 dataset is quite small, it is possible to retrieve all diaSource detections associated with an SSO -- there are only 5988.
First, confirm that the number of DiaSource table rows associated with an SSO is only 5988.
This simple TAP query counts the number of rows of the DiaSource table that are associated with a Solar System object (ssObjectId != 0).
query = "SELECT count(ssObjectId) FROM dp1.DiaSource WHERE ssObjectId != 0"
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()
assert job.phase == 'COMPLETED'
results = job.fetch_result().to_table()
del query
job.delete()
assert results['count1'] == 5988
results
Job phase is COMPLETED
| count1 |
|---|
| int64 |
| 5988 |
Create a query to retrieve all flux and flag columns for the small number of SSO detections in the DiaSource table.
Execute the query and store the results in a pandas dataframe as df_obs.
query = """SELECT ssObjectId, diaSourceId, ra, dec, x, y, visit, detector, {}, {}
FROM dp1.DiaSource WHERE ssObjectId != 0
""".format(", ".join(flux_columns), ", ".join(flag_columns))
print(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()
assert job.phase == 'COMPLETED'
df_obs = job.fetch_result().to_table().to_pandas()
del query
job.delete()
SELECT ssObjectId, diaSourceId, ra, dec, x, y, visit, detector, apFlux, apFluxErr, psfFlux, psfFluxErr, trailFlux, trailLength, scienceFlux, scienceFluxErr, isDipole, dipoleFitAttempted, pixelFlags, pixelFlags_offimage, pixelFlags_edge, pixelFlags_interpolated, pixelFlags_saturated, pixelFlags_cr, pixelFlags_bad, pixelFlags_suspect, pixelFlags_interpolatedCenter, pixelFlags_saturatedCenter, pixelFlags_crCenter, pixelFlags_suspectCenter, centroid_flag, apFlux_flag, apFlux_flag_apertureTruncated, psfFlux_flag, psfFlux_flag_noGoodPixels, psfFlux_flag_edge, forced_PsfFlux_flag, forced_PsfFlux_flag_noGoodPixels, forced_PsfFlux_flag_edge, shape_flag, shape_flag_no_pixels, shape_flag_not_contained, shape_flag_parent_source, trail_flag_edge, pixelFlags_streak, pixelFlags_streakCenter, pixelFlags_injected, pixelFlags_injectedCenter, pixelFlags_injected_template, pixelFlags_injected_templateCenter, pixelFlags_nodata, pixelFlags_nodataCenter
FROM dp1.DiaSource WHERE ssObjectId != 0
Job phase is COMPLETED
Sum the number of unique SS Objects for which photometry was returned, and calculate the mean number of detections per SSO.
temp = np.asarray(df_obs['ssObjectId'])
values, counts = np.unique(temp, return_counts=True)
print('Number of unique SS Objects in table: ', len(values))
print('Mean number of detections per SSO: ', np.round(np.mean(counts), 1))
del temp, values, counts
Number of unique SS Objects in table: 431 Mean number of detections per SSO: 13.9
Option to display the table. It will automatically truncate and not show all rows.
# df_obs
3.1. Flag values¶
The flag columns provide important context for evaluating the scientific validity of a photometric measurement for a given science use case. It is up to users to understand which flags are important for their particular analysis, and this section provides some guidance.
A flag is True when the condition in its description has been triggered.
Count of the number of times each flag was triggered in DP1 SSO photometry.
flag_counts = df_obs[flag_columns].sum()
Option to display all counts.
# flag_counts
Plot a histogram of the number of times each type of flag was triggered (for flag types with nonzero counts).
fig, ax = plt.subplots(figsize=(6, 4))
mask = flag_counts > 0
values = flag_counts[mask].sort_values(ascending=False)
ax.bar(values.index, values.values)
ax.tick_params('x', rotation=90)
ax.set_ylabel('Counts')
plt.title('DiaSource Flags')
plt.show()
del mask, values
Figure 1: Bar chart counting all flags that were triggered at least once for the DP1 SSO photometry.
3.1.1. What is a dipole?¶
Figure 1 shows that the most common flags are related to the dipole features in the DiaSource (dipoleFitAttempted, isDipole).
A "dipole" means a source has both a postive- and a negative-flux lobe, side-by-side, and is typically caused by sources that have moved slightly between the template and the science image.
For one diaSource that is flagged with isDipole, display the images to see an example of a dipole detection.
This example will use diaSource 600430116797939982.
Find the table row for that particular diaSource.
temp = df_obs.query("diaSourceId == 600430116797939982")
row = temp.index[0]
del temp
Get the x and y positions of the diaSource in the difference image, and the identifier values for the visit and detector of the difference image. Define a bounding box (bbox) that is 20 pixels on a side, centered on the x,y coordinates. Retrieve from the Butler only the pixels within the bounding box for the visit and difference image (small cutouts are often called "stamps").
xval = df_obs.loc[row, 'x']
yval = df_obs.loc[row, 'y']
visit_id = df_obs.loc[row, 'visit']
det_id = df_obs.loc[row, 'detector']
subset_box = Box2I(Point2I(xval - 10, yval-10), Extent2I(20, 20))
visit_stamp = butler.get("visit_image", visit=visit_id, detector=det_id,
parameters={'bbox': subset_box})
diff_stamp = butler.get("difference_image", visit=visit_id, detector=det_id,
parameters={'bbox': subset_box})
Instead of finding and making a stamp from the template image, recreate it from the visit and difference image stamps.
temp_stamp = deepcopy(visit_stamp)
temp_stamp.image.array = visit_stamp.image.array - diff_stamp.image.array
Plot the visit, template, and difference image stamps side-by-side as a "detection triplet".
fig, ax = plt.subplots(1, 3, figsize=(9, 4))
plt.sca(ax[0])
display0 = afw_display.Display(frame=fig)
display0.scale('linear', 'zscale')
display0.image(visit_stamp.image)
ax[0].set_title('visit')
plt.sca(ax[1])
display1 = afw_display.Display(frame=fig)
display1.scale('linear', 'zscale')
display1.image(temp_stamp.image)
ax[1].set_title('recreated template')
plt.sca(ax[2])
display2 = afw_display.Display(frame=fig)
display2.scale('linear', 'zscale')
display2.image(diff_stamp.image)
ax[2].set_title('difference')
plt.tight_layout()
plt.show()
Figure 2: The visit stamp showing the SSO detection in the science image (left), the recreated template stamp showing the SSO is also in the template (middle), and the difference stamp which shows the resulting dipole.
To view more examples of diaSource dipoles, get the diaSourceId for a different detection from the table output by the cell below, replace it in the first code cell of this sub-section, and rerun the cells above.
Option to view a few rows of the table that have the isDipole flag set.
mask = df_obs.query("isDipole == 1")
mask[:4]
| ssObjectId | diaSourceId | ra | dec | x | y | visit | detector | apFlux | apFluxErr | ... | shape_flag_parent_source | trail_flag_edge | pixelFlags_streak | pixelFlags_streakCenter | pixelFlags_injected | pixelFlags_injectedCenter | pixelFlags_injected_template | pixelFlags_injected_templateCenter | pixelFlags_nodata | pixelFlags_nodataCenter | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 21164711173436498 | 600430116797939982 | 94.616540 | -24.679264 | 2529.779954 | 2412.532074 | 2024120300082 | 6 | 4711.899902 | 712.338013 | ... | False | False | False | False | False | False | False | False | False | False |
| 1 | 21164711173436498 | 600430116932157667 | 94.616524 | -24.679273 | 201.372611 | 1778.462584 | 2024120300083 | 6 | 4791.580078 | 707.674988 | ... | False | False | False | False | False | False | False | False | False | False |
| 251 | 21163632917362253 | 600447719989837869 | 37.247400 | 6.341689 | 3748.079052 | 2931.863641 | 2024120700164 | 6 | -874.994019 | 1495.380005 | ... | False | False | False | False | False | False | False | False | False | False |
| 252 | 21163632917362253 | 600447719452966944 | 37.247439 | 6.341710 | 1264.212374 | 2707.167473 | 2024120700160 | 6 | 147.177994 | 1548.069946 | ... | False | False | False | False | False | False | False | False | False | False |
4 rows × 52 columns
Delete the image data from memory.
del visit_stamp, diff_stamp, temp_stamp
del xval, yval, visit_id, det_id, subset_box
del mask
3.2. Flux measurements¶
To get a general sense of the flux values, make histogram plots that compare the four types of flux measurements (PSF, aperture, trail, and science, as described in Section 1) for all SSO diaSource detections.
fig, ax = plt.subplots(1, 2, figsize=(8, 3), sharey=True)
plt.rcParams['hist.bins'] = 100
ax[0].hist(df_obs['psfFlux'], histtype='step', log=True, label='PSF')
ax[0].hist(df_obs['apFlux'], histtype='step', log=True, lw=2, ls='dotted', label='Aperture')
ax[0].hist(df_obs['trailFlux'], histtype='step', log=True, ls='dashed', label='Trail')
ax[0].hist(df_obs['scienceFlux'], histtype='step', log=True, lw=3, alpha=0.5, label='Science')
ax[0].legend(loc='upper right')
ax[0].set_xlabel('Flux [nJy]')
ax[0].set_ylabel('Number of SSO detections')
plt.rcParams['hist.bins'] = np.arange(-0.1e6, 3.2e6, 1e4)
ax[1].hist(df_obs['psfFlux'], histtype='step', log=True)
ax[1].hist(df_obs['apFlux'], histtype='step', lw=2, ls='dotted', log=True)
ax[1].hist(df_obs['trailFlux'], histtype='step', ls='dashed', log=True)
ax[1].hist(df_obs['scienceFlux'], histtype='step', lw=3, alpha=0.5, log=True)
ax[1].set_xlim([-0.06e6, 0.1e6])
ax[1].set_xlabel('Flux [nJy]')
plt.tight_layout()
plt.show()
Figure 3: Left, the distribution of fluxes for the four types of photometry (as labeled in the legend) shows they all cover roughly the same distribution. At right, a zoom-in on the low-flux end of the distribution shows that, as expected, the three types of photometry measured on the difference image (PSF, aperture, and trail) all include negative flux measurements.
3.3. Discrepant photometry¶
It is clear from Figure 3 (above) that there is variation between the different types of flux measurements for the same set of sources. For DP1, with only 431 unique SSOs in the table, it is very unlikely any will have been fast-moving enough to appear as a trailed source, or be non-point-like due to cometary activity. Thus it can be approximately expected that for DP1 detections of SSOs, the PSF, aperture, and trail fluxes would generally match, and that any discrepancies are associated with measurement errors (flags).
Create masked versions of the df_obs table for detections with any flags, and with the dipole flag specifically.
any_flag_mask = df_obs[flag_columns].any(axis=1)
print("Number of DP1 SSO DiaSources = {}".format(len(df_obs)))
print("Number with at least one flag = {}".format(sum(any_flag_mask)))
dipole_flag_mask = df_obs[['isDipole']].any(axis=1)
print("Number with the isDipole flag = {}".format(sum(dipole_flag_mask)))
df_obs_flagged = df_obs[any_flag_mask]
df_obs_dipole = df_obs[dipole_flag_mask]
Number of DP1 SSO DiaSources = 5988 Number with at least one flag = 539 Number with the isDipole flag = 301
Compare the aperture, trail, and science flux measurements with the PSF flux, and mark which detections were flagged.
fig, ax = plt.subplots(1, 3, figsize=(10, 3))
for a, phot in enumerate(['ap', 'trail', 'science']):
xvals = np.log10(np.abs(df_obs['psfFlux']))
yvals = np.log10(np.abs(df_obs[phot + 'Flux']))
ax[a].plot(xvals, yvals, 'o', alpha=0.1, ms=2, mew=0, color='grey', label='all')
xvals = np.log10(np.abs(df_obs_flagged['psfFlux']))
yvals = np.log10(np.abs(df_obs_flagged[phot + 'Flux']))
ax[a].plot(xvals, yvals, 'o', alpha=0.5, ms=2, mew=0, color=colors[1], label='any flag')
xvals = np.log10(np.abs(df_obs_dipole['psfFlux']))
yvals = np.log10(np.abs(df_obs_dipole[phot + 'Flux']))
ax[a].plot(xvals, yvals, 'o', alpha=0.8, ms=2, mew=0, color=colors[3], label='dipole')
ax[a].set_xlabel('log |psfFlux| [nJy]')
ax[a].set_ylabel('log |' + phot + 'Flux| [nJy]')
del xvals, yvals
leg = ax[2].legend(loc='upper left', markerscale=3, handletextpad=0)
for handle in leg.legend_handles:
handle.set_alpha(1)
plt.tight_layout()
plt.show()
Figure 4: Comparing the aperture (left), trail (middle), and science (right) flux measurements vs. the PSF flux. In order to plot in log space even though some flux measurements are negative, the absolute value of the fluxes are used. The point of this plot is not the flux values, exactly, but to show that outliers with discrepant fluxes are mostly also flagged, especially as dipoles (pink markers).
The above shows that detections for which the PSF flux differs from the other flux measurements often also have been flagged, indicating issues with their measurement. Fluxes that are affected by measurement issues should also have larger measurement uncertainties.
Plot the flux uncertainty vs. flux, for the PSF, aperture, and science flux measurements. Mark which detections were flagged.
fig, ax = plt.subplots(1, 3, figsize=(10, 3))
for a, phot in enumerate(['psf', 'ap', 'science']):
xvals = np.log10(np.abs(df_obs[phot + 'Flux']))
yvals = df_obs[phot + 'FluxErr']
ax[a].plot(xvals, yvals, 'o', alpha=0.1, ms=2, mew=0, color='grey', label='all')
xvals = np.log10(np.abs(df_obs_flagged[phot + 'Flux']))
yvals = df_obs_flagged[phot + 'FluxErr']
ax[a].plot(xvals, yvals, 'o', alpha=0.5, ms=2, mew=0, color=colors[1], label='any flag')
xvals = np.log10(np.abs(df_obs_dipole[phot + 'Flux']))
yvals = df_obs_dipole[phot + 'FluxErr']
ax[a].plot(xvals, yvals, 'o', alpha=0.8, ms=2, mew=0, color=colors[3], label='diople')
ax[a].set_xlabel('log |' + phot + 'Flux| [nJy]')
ax[a].set_ylabel(phot + 'FluxErr [nJy]')
del xvals, yvals
leg = ax[2].legend(loc='upper left', markerscale=3, handletextpad=0)
for handle in leg.legend_handles:
handle.set_alpha(1)
plt.tight_layout()
plt.show()
Figure 5: Comparing the flux error vs. the flux for each measurement type. The error vs. flux should follow a curve, with the relation set by the image quality (seeing, depth). This curve can be seen in each plot, and also that many of the outliers with higher error values have also been flagged, especially as dipoles (pink markers).
Why would the science flux error be higher for difference-image dipoles? Since the science flux is measured on the science image (the direct or visit image), instead of the difference image, it is reasonable to expect that it might not be affected by a dipole detection in the difference image. However, this is not the case and the issue does propagate to the science-image flux because the location (coordinates) at with the forced-photometry PSF flux is measured on the science image is set by the difference-image detection, and this will be affected by dipoles.
As a final investigation, compare the relative flux uncertainty (the error divided by the flux) for the science-image and difference-image PSF flux measurements.
fig = plt.figure(figsize=(4, 3))
xvals = df_obs['psfFluxErr'] / df_obs['psfFlux']
yvals = df_obs['scienceFluxErr'] / df_obs['scienceFlux']
plt.plot(xvals, yvals, 'o', alpha=0.1, ms=2, mew=0, color='grey', label='all')
xvals = df_obs_flagged['psfFluxErr'] / df_obs['psfFlux']
yvals = df_obs_flagged['scienceFluxErr'] / df_obs['scienceFlux']
plt.plot(xvals, yvals, 'o', alpha=0.5, ms=2, mew=0, color=colors[1], label='any flag')
xvals = df_obs_dipole['psfFluxErr'] / df_obs['psfFlux']
yvals = df_obs_dipole['scienceFluxErr'] / df_obs['scienceFlux']
plt.plot(xvals, yvals, 'o', alpha=0.8, ms=2, mew=0, color=colors[3], label='diople')
leg = plt.legend(loc='upper right', markerscale=3, handletextpad=0)
for handle in leg.legend_handles:
handle.set_alpha(1)
plt.tight_layout()
plt.xlabel('psfFluxErr / psfFlux')
plt.ylabel('scienceFluxErr / scienceFlux')
plt.xlim([0, 1])
plt.ylim([0, 0.25])
plt.show()
Figure 6: A scatter plot of the relative photometric uncertainty of the science-image flux vs. the difference-image PSF flux, for each SSO detection, with flag types illustrated by marker color. This plot shows that for detections flagged as dipoles (pink), the PSF fluxes are compromised as indicated by their much larger relative uncertainties. This demonstrates that the science flux is better to use for difference-image dipole detections.
3.4. Trailed flux lengths¶
Although the typical 30 second exposure time used by the LSST means that the majority of SSOs in the main asteroid belt and beyond should appear as point-like sources, near-Earth objects (NEOs) can have faster motions that could lead to trailing (appearing as a streak in a 30-second exposure).
Although for DP1 the trail flux measurement has no measurement uncertainty and is not recommended for use, this will not be the case in the future (e.g., see the Alert Production Database schema).
For trailed fluxes, the shape and size of the trail will be an important measurement to include in analyses. In DP1, given what was learned above about dipoles and flags, it's reasonable to expect the trail length, in addition to the trail flux, is affected.
Plot the distribution of the trail length values for all detections, and for flagged detections.
fig, ax = plt.subplots(1, 2, figsize=(8, 3))
ax[0].hist(df_obs['trailLength'], 100, histtype='step', density=True,
color='grey', label='all')
ax[0].hist(df_obs_flagged['trailLength'], 50, histtype='step', density=True,
color=colors[1], label='any flag')
ax[0].hist(df_obs_dipole['trailLength'], 50, histtype='step', density=True,
color=colors[3], label='dipole')
ax[0].set_xlabel('Trail length [pixels]')
ax[0].set_ylabel('Number of SSO detections')
ax[0].legend(loc='upper right')
ax[1].hist(df_obs['trailLength'], 100, histtype='step', density=True,
cumulative=True, color='grey')
ax[1].hist(df_obs_flagged['trailLength'], 50, histtype='step', density=True,
cumulative=True, color=colors[1])
ax[1].hist(df_obs_dipole['trailLength'], 50, histtype='step', density=True,
cumulative=True, color=colors[3])
ax[1].set_xlabel('Trail length [pixels]')
ax[1].set_ylabel('Cumulative SSO detections')
plt.tight_layout()
plt.show()
Figure 7: The distributions of the number of SSO detections as a function of trail length, showing that the distributions for flagged detections is skewed a bit towards longer trail lengths. As shown in Section 3.1.1, dipoles can cause an elonged total source footprint in the difference image.
4. Recommendations¶
Although this tutorial has not made use of any "truth" comparisons and only evaluated the different types of measured photometry, a few recommendations for photometry of moving objects with DP1 can be made.
- PSF: In general, for DP1 flux measurements of moving objects, the default type of photometry to use is the PSF flux measured on the difference image.
- science: For detections with the
isDipoleflag, the science-image flux may be more accurate than the difference-image PSF flux. - trail: The trail flux should not be used because it has no uncertainty.
- aperture: The aperture flux can be used, but with the caveat that as shown in Figure 4, most cases for which the aperture flux is greater than the PSF flux is not indicative of an extended object, as these detections typically have some kind of flag.
At all times, the uncertainties and flags should be considered together when including photometric points into a data analysis.