105.3. Forced photometry#
105.3. Forced 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: 2025-12-15
Repository: github.com/lsst/tutorial-notebooks
DOI: 10.11578/rubin/dc.20250909.20
Learning objective: Make forced measurements on an image for a set of coordinates.
LSST data products: deep_coadd, object table
Packages: lsst.pipe.tasks, lsst.daf.butler
Credit: Originally developed by the Rubin Community Science team working with Erfan Nourbakhsh. 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¶
In general, "forced" photometry refers to a measurement made at a fixed coordinate in an image, regardless of whether an above-threshold (SNR $>5$) source was detected at that location in that particular image. Forced photometry uses the point-spread-function (PSF) of the image, and measures the flux assuming the shape and size of the PSF at the provided coordinates.
This notebook performs forced photometry using the Data Preview 1 (DP1) data products on an input image, given a set of input coordinates (RA/Dec).
The ForcedMeasurementDriverTask serves as a convenience function, allowing users to measure sources without running a full-scale pipetask command or dealing with additional pipeline setup.
This task can measure fluxes, shapes, and other properties at known source positions (e.g., from a pre-existing catalog) on a given image, without running source detection.
For brevity, shape measurement is disabled in this tutorial.
Related tutorials: The 100-level tutorial on how to detect and measure sources. The 200-level tutorials on the PSF.
1.1. Import packages¶
Import standard python packages numpy, matplotlib, and astropy.
From the lsst package, import the TAP service (get_tap_service) and the Butler for data access, afw_display to view images, and ForcedMeasurementDriverConfig and ForcedMeasurementDriverTask: mid-level configuration and task classes used to set up and execute forced photometry.
import matplotlib.pyplot as plt
import numpy as np
import astropy.units as u
from astropy.io import ascii
from lsst.rsp import get_tap_service
from lsst.daf.butler import Butler
from lsst.pipe.tasks.measurementDriver import (
ForcedMeasurementDriverConfig,
ForcedMeasurementDriverTask,
)
1.2. Define parameters and functions¶
Create an instance of the butler, and assert that it exists.
butler = Butler("dp1", collections="LSSTComCam/DP1")
assert butler is not None
Instantiate the TAP service and assert that it exists.
service = get_tap_service("tap")
assert service is not None
Create a function called fix_flux_columns to rename output columns from the forced measurement task that are incorrectly labeled as instrument fluxes, when they are in fact not instrumental (i.e., in units of counts) but are calibrated fluxes in units of nJy.
This mistake will be fixed in future versions of the LSST Science Pipelines.
def fix_flux_columns(table):
"""Rename the columns in an output table from the ForcedMeasurementDriverTask.
This task is necessary to rename "instFlux" columns to "flux" and units from
"ct" to "nJy".
Parameters
----------
table : Astropy Table
An Astropy table resulting from executing the ForcedMeasurementDriver
Outputs
-------
table : Astropy Table
Transformed version of the input table, with columns and units updated.
"""
cols_with_instflux = [col for col in np.array(table.colnames) if 'instFlux' in col]
for col in cols_with_instflux:
new_colname = str.replace(col, 'instFlux', 'flux')
table.rename_column(col, new_colname)
table[new_colname].unit = u.nJy
return table
2. Measure objects in a deep coadd¶
Note: The fluxes in the Object catalog are already forced photometry measurements, and re-measuring them is not necessary for scientific analysis. The Object table is just used to get a set of coordinates for this tutorial.
2.1. Load an image and catalog¶
Identify coadded images overlapping a particular sky position and select one to run forced photometry on. Also load the corresponding Object table using the TAP service so that forced photometry can be run at the positions of already-detected objects.
Define the right ascension and declination as the center of the ECDFS field, and use the $r$ band.
ra = 53.1
dec = -28.1
band = 'r'
Execute a butler query for deep coadd images.
query = f"band='{band}' AND \
patch.region OVERLAPS POINT({ra}, {dec})"
coadd_img_refs = butler.query_datasets('deep_coadd', where=query,
order_by='patch')
Assert that at least one matching image was returned. Retrieve the first in the sorted list.
assert len(coadd_img_refs) > 0
ref = coadd_img_refs[0]
deep_coadd = butler.get(ref)
Query for point-like objects from the Object table in a 0.3 degree radius near the position of interest that overlap the deep coadd image's tract and patch.
query = f"""SELECT TOP 50 objectId, coord_ra, coord_dec, {band}_psfMag, {band}_psfFlux
FROM dp1.Object
WHERE CONTAINS(POINT('ICRS', coord_ra, coord_dec), CIRCLE('ICRS', {ra}, {dec}, 0.3)) = 1
AND refExtendedness < 0.5
AND {band}_psfMag < 26 AND {band}_psfMag > 17
AND tract = {ref.dataId['tract']}
AND patch = {ref.dataId['patch']}"""
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
Retrieve the search results as objtable.
This table will be used both as an input list for forced measurement, and as a comparison for the results.
assert job.phase == 'COMPLETED'
objtable = job.fetch_result().to_table()
Assert that the objtable is not empty.
assert len(objtable) > 0
Option to display the objtable.
# objtable
2.2. Run the forced measurement driver¶
Configure the forced measurement driver to use base_PsfFlux as the algorithm for the PSF flux slot, and base_TransformedCentroidFromCoord for centroids transformed from the reference catalog. Set other config parameters to disable shape measurement, avoid replacing other detected footprints with noise, and enable aperture correction for the selected flux slot. These are simply examples of how to set configuration options, and are not required to run the task.
Note: base_PsfFlux and base_TransformedCentroidFromCoord are included by default in the measurement plugins, so there's no need to add them manually to config.measurement.plugins.names. They’ll be picked up automatically.
config = ForcedMeasurementDriverConfig()
config.measurement.slots.psfFlux = "base_PsfFlux"
config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
config.measurement.slots.shape = None
config.measurement.doReplaceWithNoise = False
config.doApCorr = True
Option to examine the configuration.
# config
Create the forced photometry driver task using the configuration.
driver = ForcedMeasurementDriverTask(config=config)
Run the task using the input table of source positions and IDs, the deep_coadd image, and required parameters: column names for ID, RA, and Dec, plus a PSF footprint scaling factor (used to create synthetic footprints since detection is skipped in forced photometry).
result = driver.runFromAstropy(
objtable,
deep_coadd,
id_column_name="objectId",
ra_column_name="coord_ra",
dec_column_name="coord_dec",
psf_footprint_scaling=3.0,
)
lsst.forcedMeasurementDriver INFO: Measuring 50 sources in a single band using 'ForcedMeasurementTask'
lsst.forcedMeasurementDriver.measurement INFO: Performing forced measurement on 50 sources
lsst.forcedMeasurementDriver INFO: Applying aperture corrections to a single band
lsst.forcedMeasurementDriver.applyApCorr INFO: Applying aperture corrections to 1 instFlux fields
lsst.forcedMeasurementDriver INFO: Finished processing for a single band; output catalog has 88 fields and 50 records
Because the ForcedMeasurementDriverTask relies on code that is typically used in early stages of data processing, it assumes that fluxes are instrumental fluxes and appends the suffixes _instFlux and _instFluxErr to the output columns.
It also sets the units for these columns to "ct" (counts).
However, these assumptions about the types of fluxes are incorrect when running this task on calibrated visit and deep coadd images, whose pixel values are in nJy.
Confirm that the pixel values are in units of nJy.
deep_coadd.metadata['BUNIT']
'nJy'
Use the function defined in Section 1.2 to fix the column names. This step is temporary as the column names will be fixed in the future.
result = fix_flux_columns(result)
Examine the resulting Astropy table containing the measured sources. Each row corresponds to a record from the input table, with columns for source ID, RA, and Dec, along with additional measurement fields defined by the configuration.
result[:5]
| coord_ra | coord_dec | parent | objectId | parentObjectId | deblend_nChild | base_TransformedCentroidFromCoord_x | slot_Centroid_x | base_TransformedCentroidFromCoord_y | slot_Centroid_y | base_CircularApertureFlux_3_0_flux | base_CircularApertureFlux_3_0_fluxErr | base_CircularApertureFlux_3_0_flag | base_CircularApertureFlux_3_0_flag_apertureTruncated | base_CircularApertureFlux_3_0_flag_sincCoeffsTruncated | base_CircularApertureFlux_4_5_flux | base_CircularApertureFlux_4_5_fluxErr | base_CircularApertureFlux_4_5_flag | base_CircularApertureFlux_4_5_flag_apertureTruncated | base_CircularApertureFlux_4_5_flag_sincCoeffsTruncated | base_CircularApertureFlux_6_0_flux | base_CircularApertureFlux_6_0_fluxErr | base_CircularApertureFlux_6_0_flag | base_CircularApertureFlux_6_0_flag_apertureTruncated | base_CircularApertureFlux_6_0_flag_sincCoeffsTruncated | base_CircularApertureFlux_9_0_flux | base_CircularApertureFlux_9_0_fluxErr | base_CircularApertureFlux_9_0_flag | base_CircularApertureFlux_9_0_flag_apertureTruncated | base_CircularApertureFlux_9_0_flag_sincCoeffsTruncated | base_CircularApertureFlux_12_0_flux | base_CircularApertureFlux_12_0_fluxErr | base_CircularApertureFlux_12_0_flag | base_CircularApertureFlux_12_0_flag_apertureTruncated | base_CircularApertureFlux_17_0_flux | base_CircularApertureFlux_17_0_fluxErr | base_CircularApertureFlux_17_0_flag | base_CircularApertureFlux_17_0_flag_apertureTruncated | base_CircularApertureFlux_25_0_flux | base_CircularApertureFlux_25_0_fluxErr | base_CircularApertureFlux_25_0_flag | base_CircularApertureFlux_25_0_flag_apertureTruncated | base_CircularApertureFlux_35_0_flux | base_CircularApertureFlux_35_0_fluxErr | base_CircularApertureFlux_35_0_flag | base_CircularApertureFlux_35_0_flag_apertureTruncated | base_CircularApertureFlux_50_0_flux | base_CircularApertureFlux_50_0_fluxErr | base_CircularApertureFlux_50_0_flag | base_CircularApertureFlux_50_0_flag_apertureTruncated | base_CircularApertureFlux_70_0_flux | base_CircularApertureFlux_70_0_fluxErr | base_CircularApertureFlux_70_0_flag | base_CircularApertureFlux_70_0_flag_apertureTruncated | base_PixelFlags_flag | base_PixelFlags_flag_offimage | base_PixelFlags_flag_edge | base_PixelFlags_flag_nodata | base_PixelFlags_flag_interpolated | base_PixelFlags_flag_saturated | base_PixelFlags_flag_cr | base_PixelFlags_flag_bad | base_PixelFlags_flag_suspect | base_PixelFlags_flag_edgeCenter | base_PixelFlags_flag_nodataCenter | base_PixelFlags_flag_interpolatedCenter | base_PixelFlags_flag_saturatedCenter | base_PixelFlags_flag_crCenter | base_PixelFlags_flag_badCenter | base_PixelFlags_flag_suspectCenter | base_PixelFlags_flag_edgeCenterAll | base_PixelFlags_flag_nodataCenterAll | base_PixelFlags_flag_interpolatedCenterAll | base_PixelFlags_flag_saturatedCenterAll | base_PixelFlags_flag_crCenterAll | base_PixelFlags_flag_badCenterAll | base_PixelFlags_flag_suspectCenterAll | base_PsfFlux_flux | slot_PsfFlux_flux | base_PsfFlux_fluxErr | slot_PsfFlux_fluxErr | base_PsfFlux_area | slot_PsfFlux_area | base_PsfFlux_chi2 | slot_PsfFlux_chi2 | base_PsfFlux_npixels | slot_PsfFlux_npixels | base_PsfFlux_flag | slot_PsfFlux_flag | base_PsfFlux_flag_noGoodPixels | slot_PsfFlux_flag_noGoodPixels | base_PsfFlux_flag_edge | slot_PsfFlux_flag_edge | base_InvalidPsf_flag | base_PsfFlux_apCorr | slot_PsfFlux_apCorr | base_PsfFlux_apCorrErr | slot_PsfFlux_apCorrErr | base_PsfFlux_flag_apCorr | slot_PsfFlux_flag_apCorr |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| rad | rad | pix | pix | pix | pix | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | nJy | pix | pix | pix | pix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| float64 | float64 | int64 | int64 | int64 | int32 | float64 | float64 | float64 | float64 | float64 | float64 | bool | bool | bool | float64 | float64 | bool | bool | bool | float64 | float64 | bool | bool | bool | float64 | float64 | bool | bool | bool | float64 | float64 | bool | bool | float64 | float64 | bool | bool | float64 | float64 | bool | bool | float64 | float64 | bool | bool | float64 | float64 | bool | bool | float64 | float64 | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | bool | float64 | float64 | float64 | float64 | float32 | float32 | float32 | float32 | int32 | int32 | bool | bool | bool | bool | bool | bool | bool | float64 | float64 | float64 | float64 | bool | bool |
| 0.9297048511794681 | -0.4895890662524091 | 0 | 611254385447554142 | 0 | 0 | 12129.85016419947 | 12129.85016419947 | 5443.531496913411 | 5443.531496913411 | 137.9635772705078 | 6.553963661193848 | False | False | False | 212.96803283691406 | 9.820595741271973 | False | False | False | 255.85751342773438 | 13.152743339538574 | False | False | False | 402.291748046875 | 19.81407356262207 | False | False | False | 786.8196195960045 | 26.73253525825358 | False | False | 2432.6029037833214 | 37.89140171474067 | False | False | 4827.357851415873 | 55.644134297265545 | False | False | 6424.577063292265 | 77.9855845920656 | False | False | 7073.805814802647 | 111.31159895457795 | False | False | 8576.213145017624 | 155.74329852876846 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | 273.04504674964556 | 273.04504674964556 | 11.176410220577518 | 11.176410220577518 | 79.67906 | 79.67906 | 32709.428 | 32709.428 | 1225 | 1225 | False | False | False | False | False | False | False | 0.9938364048327201 | 0.9938364048327201 | 0.0 | 0.0 | False | False |
| 0.929557728058524 | -0.4895671850135517 | 0 | 611254385447554230 | 0 | 0 | 12263.732545559526 | 12263.732545559526 | 5466.294017031261 | 5466.294017031261 | 109.93234252929688 | 6.6442108154296875 | False | False | False | 160.69061279296875 | 9.947301864624023 | False | False | False | 180.26109313964844 | 13.327317237854004 | False | False | False | 190.79200744628906 | 20.089923858642578 | False | False | False | 143.40265300869942 | 26.993210996442993 | False | False | 174.88541135191917 | 38.2742814537264 | False | False | 343.71088138222694 | 56.254633782577834 | False | False | 790.6661548316479 | 78.74779363901956 | False | False | 573.4286175966263 | 112.49069846854228 | False | False | -1.7421963810920715 | 157.27387388022066 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | 203.4035930298747 | 203.4035930298747 | 11.279163478453675 | 11.279163478453675 | 79.21503 | 79.21503 | 1277.4198 | 1277.4198 | 1225 | 1225 | False | False | False | False | False | False | False | 0.9935566643995297 | 0.9935566643995297 | 0.0 | 0.0 | False | False |
| 0.9290484037581029 | -0.4896701564521595 | 0 | 611254385447551837 | 0 | 0 | 12727.454524789457 | 12727.454524789457 | 5360.68736463465 | 5360.68736463465 | 1611.153076171875 | 6.798088073730469 | False | False | False | 2262.928955078125 | 10.018533706665039 | False | False | False | 2611.49951171875 | 13.291465759277344 | False | False | False | 2885.21435546875 | 19.85251235961914 | False | False | False | 2962.243495941162 | 26.73441120281254 | False | False | 2892.1192423701286 | 37.72677420601975 | False | False | 2823.914140045643 | 55.38848388472099 | False | False | 2869.438548564911 | 77.5074390686532 | False | False | 3206.694326519966 | 110.64912124473167 | False | False | 8783.805091142654 | 154.99592460804413 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | 2992.890467918304 | 2992.890467918304 | 11.567128022655409 | 11.567128022655409 | 79.33693 | 79.33693 | 1246.7096 | 1246.7096 | 1225 | 1225 | False | False | False | False | False | False | False | 0.9931349709167562 | 0.9931349709167562 | 0.0 | 0.0 | False | False |
| 0.92950814749603 | -0.4895631386091517 | 0 | 611254385447554231 | 0 | 0 | 12308.856332589516 | 12308.856332589516 | 5470.5307871487985 | 5470.5307871487985 | 120.45244598388672 | 6.621951103210449 | False | False | False | 171.86965942382812 | 9.912642478942871 | False | False | False | 199.9123077392578 | 13.278959274291992 | False | False | False | 201.84889221191406 | 20.03033447265625 | False | False | False | 234.49122235178947 | 27.01674018133478 | False | False | 386.7563359737396 | 38.237997528061726 | False | False | 369.0985234081745 | 56.124860600890976 | False | False | 263.1160591542721 | 78.6504084888722 | False | False | 2648.137110263109 | 112.40803426726855 | False | False | 2735.7166201472282 | 157.4267358522609 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | 220.82155980403445 | 220.82155980403445 | 11.259585556627952 | 11.259585556627952 | 79.15674 | 79.15674 | 2168.6536 | 2168.6536 | 1225 | 1225 | False | False | False | False | False | False | False | 0.9935393230481554 | 0.9935393230481554 | 0.0 | 0.0 | False | False |
| 0.9284357235248085 | -0.48955837921319045 | 0 | 611254385447554194 | 0 | 0 | 13284.999720410628 | 13284.999720410628 | 5476.550237359781 | 5476.550237359781 | 267.4403991699219 | 6.649595737457275 | False | False | False | 374.1120910644531 | 9.9679536819458 | False | False | False | 451.2291259765625 | 13.329604148864746 | False | False | False | 487.3237609863281 | 20.0559024810791 | False | False | False | 516.9873642921448 | 27.010765596179162 | False | False | 592.7185564339161 | 38.2286436199333 | False | False | 603.4480983316898 | 56.16045435014221 | False | False | 1389.1180970072746 | 78.58883392020425 | False | False | 13701.634986788034 | 112.41619821442139 | False | False | 16783.954358518124 | 157.08954869334747 | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | 504.0876477231298 | 504.0876477231298 | 11.369714815728049 | 11.369714815728049 | 79.67882 | 79.67882 | 1257.2571 | 1257.2571 | 1225 | 1225 | False | False | False | False | False | False | False | 0.9926227225688778 | 0.9926227225688778 | 0.0 | 0.0 | False | False |
The table now includes PSF fluxes (base_psfFlux_flux) and aperture fluxes (e.g., base_CircularApertureFlux_3_0_flux for a 3-pixel aperture), among many other columns.
Most of the output columns are similar to columns that exist in the Source table -- refer to the schema for the DP1 Source table and the 200-level tutorial notebook on the Source catalog for descriptions.
Each flux measurement has an associated flux error and one or more flag columns to allow filtering of bad measurements. Additionally, there are many pixel-based flags (denoted base_PixelFlags_*) highlighting possible issues with the data for each source.
Columns that begin with slot_ are duplicates of other columns in the table.
These exist as a way of telling algorithms in the Science Pipelines which is the "preferred" measurement for things like positions, shapes, and fluxes.
Because there are multiple ways of measuring shapes (for example), this lets processing algorithms know which of these to select.
Note that the specific measurements populating (some of) the slots were configured above, for example config.measurement.slots.psfFlux = "base_PsfFlux" was specified.
The forced measurement functionality is limited to a handful of measurement plugins (for example, model-fitting algorithms will not work with this task because they require more ancillary information than it is set up to receive). Nonetheless, it offers a quick and easy way to extract measurements at arbitrary positions.
2.3. Compare forced measurements to the Object table¶
Make a plot showing the ratio of the measured PSF fluxes from the forced measurement task to the psfFlux for the same objects in the Object table. Plot these as a function of magnitude, using Astropy units to convert slot_PsfFlux_flux from the result table to AB magnitudes.
meas_mag = (result['slot_PsfFlux_flux'].value*u.nJy).to(u.ABmag)
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(meas_mag, result['slot_PsfFlux_flux']/objtable['r_psfFlux'],
'o', alpha=0.5, mew=1, mec='black', color='black')
ax.hlines(1.0, 17.3, 26.3, linestyle=':', color='gray')
ax.set_xlim(17.3, 26.3)
ax.set_xlabel('force-measured PSF magnitude')
ax.set_ylabel('flux ratio (forced / Object)')
plt.show()
Figure 1: The flux ratio of the force-measured PSF flux divided by the PSF flux from the
Objecttable, vs. the force-measured PSF magnitude (all for $r$-band).
The plot above demonstrates that the forced fluxes can sometimes be higher (brighter) than fluxes in the Object table.
This is usually due to contaminating flux from nearby objects, as deblending is not performed as part of forced measurement, but it is performed when the fluxes in the Object table are derived.
Delete the image and catalogs from memory.
del deep_coadd, objtable, result
3. Measure sources in a visit image¶
The above process of obtaining forced photometry for objects in a deep coadd image can be applied to sources in a visit image, using the same configuration parameters.
Retrieve the first $r$-band visit image that overlaps the same coordinates as defined above.
query = f"band='{band}' AND \
visit.region OVERLAPS POINT({ra}, {dec})"
visit_img_refs = butler.query_datasets('visit_image', where=query, limit=1,
order_by='visit.timespan.begin')
assert len(visit_img_refs) == 1
ref = visit_img_refs[0]
visit_image = butler.get(ref)
Query the TAP service for point-like sources detected in the visit image.
query = f"""SELECT TOP 50 sourceId, ra, dec, psfFlux
FROM dp1.Source
WHERE CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', {ra}, {dec}, 0.3)) = 1
AND extendedness < 0.5
AND visit = {ref.dataId['visit']}
AND detector = {ref.dataId['detector']}"""
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'
srctable = job.fetch_result().to_table()
assert len(srctable) > 0
Job phase is COMPLETED
Configure and run forced photometry.
The only difference from Section 2 is that when the driver is called, srctable and visit_image are passed, and the id_column_name, ra_column_name, and dec_column_name are changed, as appropriate for the Source table column names: sourceId, ra, and dec.
config = ForcedMeasurementDriverConfig()
config.measurement.slots.psfFlux = "base_PsfFlux"
config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
config.measurement.slots.shape = None
config.measurement.doReplaceWithNoise = False
config.doApCorr = True
driver = ForcedMeasurementDriverTask(config=config)
src_result = driver.runFromAstropy(
srctable,
visit_image,
id_column_name="sourceId",
ra_column_name="ra",
dec_column_name="dec",
psf_footprint_scaling=3.0,
)
src_result = fix_flux_columns(src_result)
lsst.forcedMeasurementDriver INFO: Measuring 50 sources in a single band using 'ForcedMeasurementTask'
lsst.forcedMeasurementDriver.measurement INFO: Performing forced measurement on 50 sources
lsst.forcedMeasurementDriver INFO: Applying aperture corrections to a single band
lsst.forcedMeasurementDriver.applyApCorr INFO: Applying aperture corrections to 1 instFlux fields
lsst.forcedMeasurementDriver INFO: Finished processing for a single band; output catalog has 88 fields and 50 records
Option to view 5 rows of the results table.
# src_result[:5]
del visit_image, srctable, src_result
4. Measurements for user-specified coordinates¶
Often, the motivation for doing forced photometry is to make forced measurements at a set of coordinates where no object or source has been detected with SNR $>5$ in the deep coadd or visit image (i.e., at coordinates that are not in the Object or Source tables).
These coordinates might be for subthreshold detections (e.g., SNR $<5$) or for detections from other (non-Rubin) surveys, e.g., infrared, space-based.
For this demonstration, a prepared input file of coordinates is loaded, and then forced photometry is run on a visit image.
The input file was generated by re-running source detection with config.thresholdValue = 4 on a specific $r$-band visit image (visit = 2024110800263, detector = 3), and saving the coordinates for sources with $4.0 < SNR < 4.5$ in a small region of the visit image to the file.
The sub-threshold detection magnitudes were also saved in the file.
Load the file as an astropy table and print the column names.
filepath = '/rubin/cst_repos/tutorial-notebooks-data/data/'
filename = 'dp1_105_3_subthreshold.dat'
subthresh = ascii.read(filepath+filename)
print('file columns: ', subthresh.colnames)
print('file length: ', len(subthresh))
file columns: ['id', 'ra', 'dec', 'r_mag', 'r_mag_err'] file length: 158
Option to view the loaded table.
# subthresh
Retrieve the visit image from the Butler.
visit_image = butler.get('visit_image', visit=2024110800263, detector=3)
Run the forced measurement driver with the same configuration as used in Section 3, but pass the subthresh table and use column names that match the input file.
config = ForcedMeasurementDriverConfig()
config.measurement.slots.psfFlux = "base_PsfFlux"
config.measurement.slots.centroid = "base_TransformedCentroidFromCoord"
config.measurement.slots.shape = None
config.measurement.doReplaceWithNoise = False
config.doApCorr = True
driver = ForcedMeasurementDriverTask(config=config)
src_result = driver.runFromAstropy(
subthresh,
visit_image,
id_column_name="id",
ra_column_name="ra",
dec_column_name="dec",
psf_footprint_scaling=3.0,
)
src_result = fix_flux_columns(src_result)
lsst.forcedMeasurementDriver INFO: Measuring 158 sources in a single band using 'ForcedMeasurementTask'
lsst.forcedMeasurementDriver.measurement INFO: Performing forced measurement on 158 sources
lsst.forcedMeasurementDriver INFO: Applying aperture corrections to a single band
lsst.forcedMeasurementDriver.applyApCorr INFO: Applying aperture corrections to 1 instFlux fields
lsst.forcedMeasurementDriver INFO: Finished processing for a single band; output catalog has 88 fields and 158 records
Assert that the input and output tables have the same length.
assert len(subthresh) == len(src_result)
Option to display the first five lines of the output table.
# src_result[:5]
Convert fluxes to magnitudes (and vice versa) and plot the flux ratio between the forced and sub-threshold detection fluxes as a function of the forced photometry magnitudes.
src_result['mag'] = (src_result['slot_PsfFlux_flux'].value*u.nJy).to(u.ABmag)
subthresh['r_flux'] = (subthresh['r_mag'].value*u.ABmag).to(u.nJy)
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(subthresh['r_mag'], src_result['slot_PsfFlux_flux']/subthresh['r_flux'],
'o', alpha=0.5, mew=1, mec='black', color='black')
ax.hlines(1.0, 17.3, 26.3, linestyle=':', color='gray')
ax.set_xlim(24.3, 24.5)
ax.set_xlabel('input file magnitude')
ax.set_ylabel('flux ratio (forced / input)')
plt.show()
Figure 2: The flux ratio of the force-measured PSF flux divided by the PSF flux from the original sub-threshold detection (from the input file) vs. the input file's sub-threshold detection magnitude.