Coverage for python/lsst/ap/association/diaForcedSource.py: 34%
70 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-11 08:03 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-11 08:03 +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
38class DiaForcedSourcedConfig(pexConfig.Config):
39 """Configuration for the generic DiaForcedSourcedTask class.
40 """
41 forcedMeasurement = pexConfig.ConfigurableField(
42 target=ForcedMeasurementTask,
43 doc="Subtask to force photometer DiaObjects in the direct and "
44 "difference images.",
45 )
46 dropColumns = pexConfig.ListField(
47 dtype=str,
48 doc="Columns produced in forced measurement that can be dropped upon "
49 "creation and storage of the final pandas data.",
50 )
52 def setDefaults(self):
53 self.forcedMeasurement.plugins = ["base_TransformedCentroidFromCoord",
54 "base_PsfFlux"]
55 self.forcedMeasurement.doReplaceWithNoise = False
56 self.forcedMeasurement.copyColumns = {
57 "id": "diaObjectId",
58 "coord_ra": "coord_ra",
59 "coord_dec": "coord_dec"}
60 self.forcedMeasurement.slots.centroid = "base_TransformedCentroidFromCoord"
61 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux"
62 self.forcedMeasurement.slots.shape = None
63 self.dropColumns = ['coord_ra', 'coord_dec', 'parent',
64 'base_TransformedCentroidFromCoord_x',
65 'base_TransformedCentroidFromCoord_y',
66 'base_PsfFlux_instFlux',
67 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area',
68 'slot_PsfFlux_area', 'base_PsfFlux_flag',
69 'slot_PsfFlux_flag',
70 'base_PsfFlux_flag_noGoodPixels',
71 'slot_PsfFlux_flag_noGoodPixels',
72 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge',
73 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2',
74 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels',
75 ]
78class DiaForcedSourceTask(pipeBase.Task):
79 """Task for measuring and storing forced sources at DiaObject locations
80 in both difference and direct images.
81 """
82 ConfigClass = DiaForcedSourcedConfig
83 _DefaultName = "diaForcedSource"
85 def __init__(self, **kwargs):
86 pipeBase.Task.__init__(self, **kwargs)
87 self.makeSubtask("forcedMeasurement",
88 refSchema=afwTable.SourceTable.makeMinimalSchema())
90 @pipeBase.timeMethod
91 def run(self,
92 dia_objects,
93 updatedDiaObjectIds,
94 expIdBits,
95 exposure,
96 diffim):
97 """Measure forced sources on the direct and difference images.
99 Parameters
100 ----------
101 dia_objects : `pandas.DataFrame`
102 Catalog of previously observed and newly created DiaObjects
103 contained within the difference and direct images. DiaObjects
104 must be indexed on the ``diaObjectId`` column.
105 updatedDiaObjectIds : `numpy.ndarray`
106 Array of diaObjectIds that were updated during this dia processing.
107 Used to assure that the pipeline includes all diaObjects that were
108 updated in case one falls on the edge of the CCD.
109 expIdBits : `int`
110 Bit length of the exposure id.
111 exposure : `lsst.afw.image.Exposure`
112 Direct image exposure.
113 diffim : `lsst.afw.image.Exposure`
114 Difference image.
116 Returns
117 -------
118 output_forced_sources : `pandas.DataFrame`
119 Catalog of calibrated forced photometered fluxes on both the
120 difference and direct images at DiaObject locations.
121 """
123 afw_dia_objects = self._convert_from_pandas(dia_objects)
125 idFactoryDiff = afwTable.IdFactory.makeSource(
126 diffim.getInfo().getVisitInfo().getExposureId(),
127 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
129 diffForcedSources = self.forcedMeasurement.generateMeasCat(
130 diffim,
131 afw_dia_objects,
132 diffim.getWcs(),
133 idFactory=idFactoryDiff)
134 self.forcedMeasurement.run(
135 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
137 directForcedSources = self.forcedMeasurement.generateMeasCat(
138 exposure,
139 afw_dia_objects,
140 exposure.getWcs())
141 self.forcedMeasurement.run(
142 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
144 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
145 directForcedSources,
146 diffim,
147 exposure)
149 output_forced_sources = self._trim_to_exposure(output_forced_sources,
150 updatedDiaObjectIds,
151 exposure)
152 return output_forced_sources.set_index(
153 ["diaObjectId", "diaForcedSourceId"],
154 drop=False)
156 def _convert_from_pandas(self, input_objects):
157 """Create minimal schema SourceCatalog from a pandas DataFrame.
159 We need a catalog of this type to run within the forced measurement
160 subtask.
162 Parameters
163 ----------
164 input_objects : `pandas.DataFrame`
165 DiaObjects with locations and ids. ``
167 Returns
168 -------
169 outputCatalog : `lsst.afw.table.SourceTable`
170 Output catalog with minimal schema.
171 """
172 schema = afwTable.SourceTable.makeMinimalSchema()
174 outputCatalog = afwTable.SourceCatalog(schema)
175 outputCatalog.reserve(len(input_objects))
177 for obj_id, df_row in input_objects.iterrows():
178 outputRecord = outputCatalog.addNew()
179 outputRecord.setId(obj_id)
180 outputRecord.setCoord(
181 geom.SpherePoint(df_row["ra"],
182 df_row["decl"],
183 geom.degrees))
184 return outputCatalog
186 def _calibrate_and_merge(self,
187 diff_sources,
188 direct_sources,
189 diff_exp,
190 direct_exp):
191 """Take the two output catalogs from the ForcedMeasurementTasks and
192 calibrate, combine, and convert them to Pandas.
194 Parameters
195 ----------
196 diff_sources : `lsst.afw.table.SourceTable`
197 Catalog with PsFluxes measured on the difference image.
198 direct_sources : `lsst.afw.table.SourceTable`
199 Catalog with PsfFluxes measured on the direct (calexp) image.
200 diff_exp : `lsst.afw.image.Exposure`
201 Difference exposure ``diff_sources`` were measured on.
202 direct_exp : `lsst.afw.image.Exposure`
203 Direct (calexp) exposure ``direct_sources`` were measured on.
205 Returns
206 -------
207 output_catalog : `pandas.DataFrame`
208 Catalog calibrated diaForcedSources.
209 """
210 diff_calib = diff_exp.getPhotoCalib()
211 direct_calib = direct_exp.getPhotoCalib()
213 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
214 "slot_PsfFlux")
215 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
216 "slot_PsfFlux")
218 output_catalog = diff_sources.asAstropy().to_pandas()
219 output_catalog.rename(columns={"id": "diaForcedSourceId",
220 "slot_PsfFlux_instFlux": "psFlux",
221 "slot_PsfFlux_instFluxErr": "psFluxErr",
222 "slot_Centroid_x": "x",
223 "slot_Centroid_y": "y"},
224 inplace=True)
225 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
226 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
228 output_catalog["totFlux"] = direct_fluxes[:, 0]
229 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
231 visit_info = direct_exp.getInfo().getVisitInfo()
232 ccdVisitId = visit_info.getExposureId()
233 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
234 output_catalog["ccdVisitId"] = ccdVisitId
235 output_catalog["midPointTai"] = midPointTaiMJD
236 output_catalog["filterName"] = diff_exp.getFilterLabel().bandLabel
238 # Drop superfluous columns from output DataFrame.
239 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
241 return output_catalog
243 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure):
244 """Remove DiaForcedSources that are outside of the bounding box region.
246 Paramters
247 ---------
248 catalog : `pandas.DataFrame`
249 DiaForcedSources to check against the exposure bounding box.
250 updatedDiaObjectIds : `numpy.ndarray`
251 Array of diaObjectIds that were updated during this dia processing.
252 Used to assure that the pipeline includes all diaObjects that were
253 updated in case one falls on the edge of the CCD.
254 exposure : `lsst.afw.image.Exposure`
255 Exposure to check against.
257 Returns
258 -------
259 output : `pandas.DataFrame`
260 DataFrame trimmed to only the objects within the exposure bounding
261 box.
262 """
263 bbox = geom.Box2D(exposure.getBBox())
265 xS = catalog.loc[:, "x"]
266 yS = catalog.loc[:, "y"]
268 return catalog[
269 np.logical_or(bbox.contains(xS, yS),
270 np.isin(catalog.loc[:, "diaObjectId"],
271 updatedDiaObjectIds))]