Coverage for python/lsst/ap/association/diaForcedSource.py: 32%
71 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 10:17 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 10:17 +0000
1# This file is part of ap_association.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""Methods for force photometering direct and difference images at DiaObject
23locations.
24"""
26__all__ = ["DiaForcedSourceTask", "DiaForcedSourcedConfig"]
28import numpy as np
30import lsst.afw.table as afwTable
31from lsst.daf.base import DateTime
32import lsst.geom as geom
33from lsst.meas.base import ForcedMeasurementTask
34import lsst.pex.config as pexConfig
35import lsst.pipe.base as pipeBase
36from lsst.utils.timer import timeMethod
39class DiaForcedSourcedConfig(pexConfig.Config):
40 """Configuration for the generic DiaForcedSourcedTask class.
41 """
42 forcedMeasurement = pexConfig.ConfigurableField(
43 target=ForcedMeasurementTask,
44 doc="Subtask to force photometer DiaObjects in the direct and "
45 "difference images.",
46 )
47 dropColumns = pexConfig.ListField(
48 dtype=str,
49 doc="Columns produced in forced measurement that can be dropped upon "
50 "creation and storage of the final pandas data.",
51 )
53 def setDefaults(self):
54 self.forcedMeasurement.plugins = ["base_TransformedCentroidFromCoord",
55 "base_PsfFlux"]
56 self.forcedMeasurement.doReplaceWithNoise = False
57 self.forcedMeasurement.copyColumns = {
58 "id": "diaObjectId",
59 "coord_ra": "coord_ra",
60 "coord_dec": "coord_dec"}
61 self.forcedMeasurement.slots.centroid = "base_TransformedCentroidFromCoord"
62 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux"
63 self.forcedMeasurement.slots.shape = None
64 self.dropColumns = ['coord_ra', 'coord_dec', 'parent',
65 'base_TransformedCentroidFromCoord_x',
66 'base_TransformedCentroidFromCoord_y',
67 'base_PsfFlux_instFlux',
68 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area',
69 'slot_PsfFlux_area', 'base_PsfFlux_flag',
70 'slot_PsfFlux_flag',
71 'base_PsfFlux_flag_noGoodPixels',
72 'slot_PsfFlux_flag_noGoodPixels',
73 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge',
74 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2',
75 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels',
76 ]
79class DiaForcedSourceTask(pipeBase.Task):
80 """Task for measuring and storing forced sources at DiaObject locations
81 in both difference and direct images.
82 """
83 ConfigClass = DiaForcedSourcedConfig
84 _DefaultName = "diaForcedSource"
86 def __init__(self, **kwargs):
87 pipeBase.Task.__init__(self, **kwargs)
88 self.makeSubtask("forcedMeasurement",
89 refSchema=afwTable.SourceTable.makeMinimalSchema())
91 @timeMethod
92 def run(self,
93 dia_objects,
94 updatedDiaObjectIds,
95 expIdBits,
96 exposure,
97 diffim):
98 """Measure forced sources on the direct and difference images.
100 Parameters
101 ----------
102 dia_objects : `pandas.DataFrame`
103 Catalog of previously observed and newly created DiaObjects
104 contained within the difference and direct images. DiaObjects
105 must be indexed on the ``diaObjectId`` column.
106 updatedDiaObjectIds : `numpy.ndarray`
107 Array of diaObjectIds that were updated during this dia processing.
108 Used to assure that the pipeline includes all diaObjects that were
109 updated in case one falls on the edge of the CCD.
110 expIdBits : `int`
111 Bit length of the exposure id.
112 exposure : `lsst.afw.image.Exposure`
113 Direct image exposure.
114 diffim : `lsst.afw.image.Exposure`
115 Difference image.
117 Returns
118 -------
119 output_forced_sources : `pandas.DataFrame`
120 Catalog of calibrated forced photometered fluxes on both the
121 difference and direct images at DiaObject locations.
122 """
124 afw_dia_objects = self._convert_from_pandas(dia_objects)
126 idFactoryDiff = afwTable.IdFactory.makeSource(
127 diffim.info.id,
128 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
130 diffForcedSources = self.forcedMeasurement.generateMeasCat(
131 diffim,
132 afw_dia_objects,
133 diffim.getWcs(),
134 idFactory=idFactoryDiff)
135 self.forcedMeasurement.run(
136 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
138 directForcedSources = self.forcedMeasurement.generateMeasCat(
139 exposure,
140 afw_dia_objects,
141 exposure.getWcs())
142 self.forcedMeasurement.run(
143 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
145 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
146 directForcedSources,
147 diffim,
148 exposure)
150 output_forced_sources = self._trim_to_exposure(output_forced_sources,
151 updatedDiaObjectIds,
152 exposure)
153 return output_forced_sources.set_index(
154 ["diaObjectId", "diaForcedSourceId"],
155 drop=False)
157 def _convert_from_pandas(self, input_objects):
158 """Create minimal schema SourceCatalog from a pandas DataFrame.
160 We need a catalog of this type to run within the forced measurement
161 subtask.
163 Parameters
164 ----------
165 input_objects : `pandas.DataFrame`
166 DiaObjects with locations and ids. ``
168 Returns
169 -------
170 outputCatalog : `lsst.afw.table.SourceTable`
171 Output catalog with minimal schema.
172 """
173 schema = afwTable.SourceTable.makeMinimalSchema()
175 outputCatalog = afwTable.SourceCatalog(schema)
176 outputCatalog.reserve(len(input_objects))
178 for obj_id, df_row in input_objects.iterrows():
179 outputRecord = outputCatalog.addNew()
180 outputRecord.setId(obj_id)
181 outputRecord.setCoord(
182 geom.SpherePoint(df_row["ra"],
183 df_row["decl"],
184 geom.degrees))
185 return outputCatalog
187 def _calibrate_and_merge(self,
188 diff_sources,
189 direct_sources,
190 diff_exp,
191 direct_exp):
192 """Take the two output catalogs from the ForcedMeasurementTasks and
193 calibrate, combine, and convert them to Pandas.
195 Parameters
196 ----------
197 diff_sources : `lsst.afw.table.SourceTable`
198 Catalog with PsFluxes measured on the difference image.
199 direct_sources : `lsst.afw.table.SourceTable`
200 Catalog with PsfFluxes measured on the direct (calexp) image.
201 diff_exp : `lsst.afw.image.Exposure`
202 Difference exposure ``diff_sources`` were measured on.
203 direct_exp : `lsst.afw.image.Exposure`
204 Direct (calexp) exposure ``direct_sources`` were measured on.
206 Returns
207 -------
208 output_catalog : `pandas.DataFrame`
209 Catalog calibrated diaForcedSources.
210 """
211 diff_calib = diff_exp.getPhotoCalib()
212 direct_calib = direct_exp.getPhotoCalib()
214 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
215 "slot_PsfFlux")
216 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
217 "slot_PsfFlux")
219 output_catalog = diff_sources.asAstropy().to_pandas()
220 output_catalog.rename(columns={"id": "diaForcedSourceId",
221 "slot_PsfFlux_instFlux": "psFlux",
222 "slot_PsfFlux_instFluxErr": "psFluxErr",
223 "slot_Centroid_x": "x",
224 "slot_Centroid_y": "y"},
225 inplace=True)
226 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
227 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
229 output_catalog["totFlux"] = direct_fluxes[:, 0]
230 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
232 visit_info = direct_exp.getInfo().getVisitInfo()
233 ccdVisitId = direct_exp.info.id
234 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
235 output_catalog["ccdVisitId"] = ccdVisitId
236 output_catalog["midPointTai"] = midPointTaiMJD
237 output_catalog["filterName"] = diff_exp.getFilter().bandLabel
239 # Drop superfluous columns from output DataFrame.
240 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
242 return output_catalog
244 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure):
245 """Remove DiaForcedSources that are outside of the bounding box region.
247 Paramters
248 ---------
249 catalog : `pandas.DataFrame`
250 DiaForcedSources to check against the exposure bounding box.
251 updatedDiaObjectIds : `numpy.ndarray`
252 Array of diaObjectIds that were updated during this dia processing.
253 Used to assure that the pipeline includes all diaObjects that were
254 updated in case one falls on the edge of the CCD.
255 exposure : `lsst.afw.image.Exposure`
256 Exposure to check against.
258 Returns
259 -------
260 output : `pandas.DataFrame`
261 DataFrame trimmed to only the objects within the exposure bounding
262 box.
263 """
264 bbox = geom.Box2D(exposure.getBBox())
266 xS = catalog.loc[:, "x"]
267 yS = catalog.loc[:, "y"]
269 return catalog[
270 np.logical_or(bbox.contains(xS, yS),
271 np.isin(catalog.loc[:, "diaObjectId"],
272 updatedDiaObjectIds))]