Coverage for python/lsst/ap/association/diaForcedSource.py: 29%
76 statements
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:58 -0700
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:58 -0700
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 warnings
29import numpy as np
31import lsst.afw.table as afwTable
32from lsst.daf.base import DateTime
33import lsst.geom as geom
34from lsst.meas.base import ForcedMeasurementTask
35import lsst.pex.config as pexConfig
36import lsst.pipe.base as pipeBase
37from lsst.utils.timer import timeMethod
40class DiaForcedSourcedConfig(pexConfig.Config):
41 """Configuration for the generic DiaForcedSourcedTask class.
42 """
43 forcedMeasurement = pexConfig.ConfigurableField(
44 target=ForcedMeasurementTask,
45 doc="Subtask to force photometer DiaObjects in the direct and "
46 "difference images.",
47 )
48 dropColumns = pexConfig.ListField(
49 dtype=str,
50 doc="Columns produced in forced measurement that can be dropped upon "
51 "creation and storage of the final pandas data.",
52 )
54 def setDefaults(self):
55 self.forcedMeasurement.plugins = ["base_TransformedCentroidFromCoord",
56 "base_PsfFlux"]
57 self.forcedMeasurement.doReplaceWithNoise = False
58 self.forcedMeasurement.copyColumns = {
59 "id": "diaObjectId",
60 "coord_ra": "coord_ra",
61 "coord_dec": "coord_dec"}
62 self.forcedMeasurement.slots.centroid = "base_TransformedCentroidFromCoord"
63 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux"
64 self.forcedMeasurement.slots.shape = None
65 self.dropColumns = ['coord_ra', 'coord_dec', 'parent',
66 'base_TransformedCentroidFromCoord_x',
67 'base_TransformedCentroidFromCoord_y',
68 'base_PsfFlux_instFlux',
69 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area',
70 'slot_PsfFlux_area', 'base_PsfFlux_flag',
71 'slot_PsfFlux_flag',
72 'base_PsfFlux_flag_noGoodPixels',
73 'slot_PsfFlux_flag_noGoodPixels',
74 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge',
75 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2',
76 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels',
77 ]
80class DiaForcedSourceTask(pipeBase.Task):
81 """Task for measuring and storing forced sources at DiaObject locations
82 in both difference and direct images.
83 """
84 ConfigClass = DiaForcedSourcedConfig
85 _DefaultName = "diaForcedSource"
87 def __init__(self, **kwargs):
88 pipeBase.Task.__init__(self, **kwargs)
89 self.makeSubtask("forcedMeasurement",
90 refSchema=afwTable.SourceTable.makeMinimalSchema())
92 @timeMethod
93 def run(self,
94 dia_objects,
95 updatedDiaObjectIds,
96 expIdBits, # TODO: remove on DM-38687.
97 exposure,
98 diffim,
99 idGenerator=None):
100 """Measure forced sources on the direct and difference images.
102 Parameters
103 ----------
104 dia_objects : `pandas.DataFrame`
105 Catalog of previously observed and newly created DiaObjects
106 contained within the difference and direct images. DiaObjects
107 must be indexed on the ``diaObjectId`` column.
108 updatedDiaObjectIds : `numpy.ndarray`
109 Array of diaObjectIds that were updated during this dia processing.
110 Used to assure that the pipeline includes all diaObjects that were
111 updated in case one falls on the edge of the CCD.
112 expIdBits : `int`
113 Bit length of the exposure id. Deprecated in favor of
114 ``idGenerator``, and ignored if that is present. Pass `None`
115 explicitly to avoid a deprecation warning (a default is impossible
116 given that later positional arguments are not defaulted).
117 exposure : `lsst.afw.image.Exposure`
118 Direct image exposure.
119 diffim : `lsst.afw.image.Exposure`
120 Difference image.
121 idGenerator : `lsst.meas.base.IdGenerator`, optional
122 Object that generates source IDs and random number generator seeds.
123 Will be required after ``expIdBits`` is removed.
125 Returns
126 -------
127 output_forced_sources : `pandas.DataFrame`
128 Catalog of calibrated forced photometered fluxes on both the
129 difference and direct images at DiaObject locations.
130 """
132 afw_dia_objects = self._convert_from_pandas(dia_objects)
134 if expIdBits is not None:
135 warnings.warn(
136 "'expIdBits' argument is deprecated in favor of 'idGenerator'; will be removed after v27.",
137 category=FutureWarning,
138 )
140 if idGenerator is None:
141 idFactoryDiff = afwTable.IdFactory.makeSource(
142 diffim.info.id,
143 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
144 else:
145 idFactoryDiff = idGenerator.make_table_id_factory()
147 diffForcedSources = self.forcedMeasurement.generateMeasCat(
148 diffim,
149 afw_dia_objects,
150 diffim.getWcs(),
151 idFactory=idFactoryDiff)
152 self.forcedMeasurement.run(
153 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
155 directForcedSources = self.forcedMeasurement.generateMeasCat(
156 exposure,
157 afw_dia_objects,
158 exposure.getWcs(),
159 idFactory=idFactoryDiff)
160 self.forcedMeasurement.run(
161 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
163 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
164 directForcedSources,
165 diffim,
166 exposure)
168 output_forced_sources = self._trim_to_exposure(output_forced_sources,
169 updatedDiaObjectIds,
170 exposure)
171 return output_forced_sources.set_index(
172 ["diaObjectId", "diaForcedSourceId"],
173 drop=False)
175 def _convert_from_pandas(self, input_objects):
176 """Create minimal schema SourceCatalog from a pandas DataFrame.
178 We need a catalog of this type to run within the forced measurement
179 subtask.
181 Parameters
182 ----------
183 input_objects : `pandas.DataFrame`
184 DiaObjects with locations and ids. ``
186 Returns
187 -------
188 outputCatalog : `lsst.afw.table.SourceTable`
189 Output catalog with minimal schema.
190 """
191 schema = afwTable.SourceTable.makeMinimalSchema()
193 outputCatalog = afwTable.SourceCatalog(schema)
194 outputCatalog.reserve(len(input_objects))
196 for obj_id, df_row in input_objects.iterrows():
197 outputRecord = outputCatalog.addNew()
198 outputRecord.setId(obj_id)
199 outputRecord.setCoord(
200 geom.SpherePoint(df_row["ra"],
201 df_row["decl"],
202 geom.degrees))
203 return outputCatalog
205 def _calibrate_and_merge(self,
206 diff_sources,
207 direct_sources,
208 diff_exp,
209 direct_exp):
210 """Take the two output catalogs from the ForcedMeasurementTasks and
211 calibrate, combine, and convert them to Pandas.
213 Parameters
214 ----------
215 diff_sources : `lsst.afw.table.SourceTable`
216 Catalog with PsFluxes measured on the difference image.
217 direct_sources : `lsst.afw.table.SourceTable`
218 Catalog with PsfFluxes measured on the direct (calexp) image.
219 diff_exp : `lsst.afw.image.Exposure`
220 Difference exposure ``diff_sources`` were measured on.
221 direct_exp : `lsst.afw.image.Exposure`
222 Direct (calexp) exposure ``direct_sources`` were measured on.
224 Returns
225 -------
226 output_catalog : `pandas.DataFrame`
227 Catalog calibrated diaForcedSources.
228 """
229 diff_calib = diff_exp.getPhotoCalib()
230 direct_calib = direct_exp.getPhotoCalib()
232 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
233 "slot_PsfFlux")
234 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
235 "slot_PsfFlux")
237 output_catalog = diff_sources.asAstropy().to_pandas()
238 output_catalog.rename(columns={"id": "diaForcedSourceId",
239 "slot_PsfFlux_instFlux": "psFlux",
240 "slot_PsfFlux_instFluxErr": "psFluxErr",
241 "slot_Centroid_x": "x",
242 "slot_Centroid_y": "y"},
243 inplace=True)
244 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
245 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
247 output_catalog["totFlux"] = direct_fluxes[:, 0]
248 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
250 visit_info = direct_exp.getInfo().getVisitInfo()
251 ccdVisitId = direct_exp.info.id
252 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
253 output_catalog["ccdVisitId"] = ccdVisitId
254 output_catalog["midPointTai"] = midPointTaiMJD
255 output_catalog["filterName"] = diff_exp.getFilter().bandLabel
257 # Drop superfluous columns from output DataFrame.
258 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
260 return output_catalog
262 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure):
263 """Remove DiaForcedSources that are outside of the bounding box region.
265 Paramters
266 ---------
267 catalog : `pandas.DataFrame`
268 DiaForcedSources to check against the exposure bounding box.
269 updatedDiaObjectIds : `numpy.ndarray`
270 Array of diaObjectIds that were updated during this dia processing.
271 Used to assure that the pipeline includes all diaObjects that were
272 updated in case one falls on the edge of the CCD.
273 exposure : `lsst.afw.image.Exposure`
274 Exposure to check against.
276 Returns
277 -------
278 output : `pandas.DataFrame`
279 DataFrame trimmed to only the objects within the exposure bounding
280 box.
281 """
282 bbox = geom.Box2D(exposure.getBBox())
284 xS = catalog.loc[:, "x"]
285 yS = catalog.loc[:, "y"]
287 return catalog[
288 np.logical_or(bbox.contains(xS, yS),
289 np.isin(catalog.loc[:, "diaObjectId"],
290 updatedDiaObjectIds))]