Coverage for python/lsst/ap/association/diaForcedSource.py : 35%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.pluginRegistry import register
34from lsst.meas.base import (
35 ForcedMeasurementTask,
36 ForcedTransformedCentroidConfig,
37 ForcedTransformedCentroidPlugin)
38import lsst.pex.config as pexConfig
39import lsst.pipe.base as pipeBase
42class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig):
43 """Configuration for the forced transformed coord algorithm.
44 """
45 pass
48@register("ap_assoc_TransformedCentroid")
49class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin):
50 """Record the transformation of the reference catalog coord.
51 The coord recorded in the reference catalog is tranformed to the
52 measurement coordinate system and stored.
54 Parameters
55 ----------
56 config : `ForcedTransformedCentroidFromCoordConfig`
57 Plugin configuration
58 name : `str`
59 Plugin name
60 schemaMapper : `lsst.afw.table.SchemaMapper`
61 A mapping from reference catalog fields to output
62 catalog fields. Output fields are added to the output schema.
63 metadata : `lsst.daf.base.PropertySet`
64 Plugin metadata that will be attached to the output catalog.
66 Notes
67 -----
68 This can be used as the slot centroid in forced measurement when only a
69 reference coord exits, allowing subsequent measurements to simply refer to
70 the slot value just as they would in single-frame measurement.
71 """
73 ConfigClass = ForcedTransformedCentroidFromCoordConfig
75 def measure(self, measRecord, exposure, refRecord, refWcs):
76 targetWcs = exposure.getWcs()
78 targetPos = targetWcs.skyToPixel(refRecord.getCoord())
79 measRecord.set(self.centroidKey, targetPos)
81 if self.flagKey is not None:
82 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
85class DiaForcedSourcedConfig(pexConfig.Config):
86 """Configuration for the generic DiaForcedSourcedTask class.
87 """
88 forcedMeasurement = pexConfig.ConfigurableField(
89 target=ForcedMeasurementTask,
90 doc="Subtask to force photometer DiaObjects in the direct and "
91 "difference images.",
92 )
93 dropColumns = pexConfig.ListField(
94 dtype=str,
95 doc="Columns produced in forced measurement that can be dropped upon "
96 "creation and storage of the final pandas data.",
97 )
99 def setDefaults(self):
100 self.forcedMeasurement.plugins = ["ap_assoc_TransformedCentroid",
101 "base_PsfFlux"]
102 self.forcedMeasurement.doReplaceWithNoise = False
103 self.forcedMeasurement.copyColumns = {
104 "id": "diaObjectId",
105 "coord_ra": "coord_ra",
106 "coord_dec": "coord_dec"}
107 self.forcedMeasurement.slots.centroid = "ap_assoc_TransformedCentroid"
108 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux"
109 self.forcedMeasurement.slots.shape = None
110 self.dropColumns = ['coord_ra', 'coord_dec', 'parent',
111 'ap_assoc_TransformedCentroid_x',
112 'ap_assoc_TransformedCentroid_y',
113 'base_PsfFlux_instFlux',
114 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area',
115 'slot_PsfFlux_area', 'base_PsfFlux_flag',
116 'slot_PsfFlux_flag',
117 'base_PsfFlux_flag_noGoodPixels',
118 'slot_PsfFlux_flag_noGoodPixels',
119 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge']
122class DiaForcedSourceTask(pipeBase.Task):
123 """Task for measuring and storing forced sources at DiaObject locations
124 in both difference and direct images.
125 """
126 ConfigClass = DiaForcedSourcedConfig
127 _DefaultName = "diaForcedSource"
129 def __init__(self, **kwargs):
130 pipeBase.Task.__init__(self, **kwargs)
131 self.makeSubtask("forcedMeasurement",
132 refSchema=afwTable.SourceTable.makeMinimalSchema())
134 @pipeBase.timeMethod
135 def run(self,
136 dia_objects,
137 updatedDiaObjectIds,
138 expIdBits,
139 exposure,
140 diffim):
141 """Measure forced sources on the direct and difference images.
143 Parameters
144 ----------
145 dia_objects : `pandas.DataFrame`
146 Catalog of previously observed and newly created DiaObjects
147 contained within the difference and direct images. DiaObjects
148 must be indexed on the ``diaObjectId`` column.
149 updatedDiaObjectIds : `numpy.ndarray`
150 Array of diaObjectIds that were updated during this dia processing.
151 Used to assure that the pipeline includes all diaObjects that were
152 updated in case one falls on the edge of the CCD.
153 expIdBits : `int`
154 Bit length of the exposure id.
155 exposure : `lsst.afw.image.Exposure`
156 Direct image exposure.
157 diffim : `lsst.afw.image.Exposure`
158 Difference image.
160 Returns
161 -------
162 output_forced_sources : `pandas.DataFrame`
163 Catalog of calibrated forced photometered fluxes on both the
164 difference and direct images at DiaObject locations.
165 """
167 afw_dia_objects = self._convert_from_pandas(dia_objects)
169 idFactoryDiff = afwTable.IdFactory.makeSource(
170 diffim.getInfo().getVisitInfo().getExposureId(),
171 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
173 diffForcedSources = self.forcedMeasurement.generateMeasCat(
174 diffim,
175 afw_dia_objects,
176 diffim.getWcs(),
177 idFactory=idFactoryDiff)
178 self.forcedMeasurement.run(
179 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
181 directForcedSources = self.forcedMeasurement.generateMeasCat(
182 exposure,
183 afw_dia_objects,
184 exposure.getWcs())
185 self.forcedMeasurement.run(
186 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
188 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
189 directForcedSources,
190 diffim,
191 exposure)
193 output_forced_sources = self._trim_to_exposure(output_forced_sources,
194 updatedDiaObjectIds,
195 exposure)
196 return output_forced_sources.set_index(
197 ["diaObjectId", "diaForcedSourceId"],
198 drop=False)
200 def _convert_from_pandas(self, input_objects):
201 """Create minimal schema SourceCatalog from a pandas DataFrame.
203 We need a catalog of this type to run within the forced measurement
204 subtask.
206 Parameters
207 ----------
208 input_objects : `pandas.DataFrame`
209 DiaObjects with locations and ids. ``
211 Returns
212 -------
213 outputCatalog : `lsst.afw.table.SourceTable`
214 Output catalog with minimal schema.
215 """
216 schema = afwTable.SourceTable.makeMinimalSchema()
218 outputCatalog = afwTable.SourceCatalog(schema)
219 outputCatalog.reserve(len(input_objects))
221 for obj_id, df_row in input_objects.iterrows():
222 outputRecord = outputCatalog.addNew()
223 outputRecord.setId(obj_id)
224 outputRecord.setCoord(
225 geom.SpherePoint(df_row["ra"],
226 df_row["decl"],
227 geom.degrees))
228 return outputCatalog
230 def _calibrate_and_merge(self,
231 diff_sources,
232 direct_sources,
233 diff_exp,
234 direct_exp):
235 """Take the two output catalogs from the ForcedMeasurementTasks and
236 calibrate, combine, and convert them to Pandas.
238 Parameters
239 ----------
240 diff_sources : `lsst.afw.table.SourceTable`
241 Catalog with PsFluxes measured on the difference image.
242 direct_sources : `lsst.afw.table.SourceTable`
243 Catalog with PsfFluxes measured on the direct (calexp) image.
244 diff_exp : `lsst.afw.image.Exposure`
245 Difference exposure ``diff_sources`` were measured on.
246 direct_exp : `lsst.afw.image.Exposure`
247 Direct (calexp) exposure ``direct_sources`` were measured on.
249 Returns
250 -------
251 output_catalog : `pandas.DataFrame`
252 Catalog calibrated diaForcedSources.
253 """
254 diff_calib = diff_exp.getPhotoCalib()
255 direct_calib = direct_exp.getPhotoCalib()
257 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
258 "slot_PsfFlux")
259 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
260 "slot_PsfFlux")
262 output_catalog = diff_sources.asAstropy().to_pandas()
263 output_catalog.rename(columns={"id": "diaForcedSourceId",
264 "slot_PsfFlux_instFlux": "psFlux",
265 "slot_PsfFlux_instFluxErr": "psFluxErr",
266 "slot_Centroid_x": "x",
267 "slot_Centroid_y": "y"},
268 inplace=True)
269 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
270 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
272 output_catalog["totFlux"] = direct_fluxes[:, 0]
273 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
275 visit_info = direct_exp.getInfo().getVisitInfo()
276 ccdVisitId = visit_info.getExposureId()
277 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
278 output_catalog["ccdVisitId"] = ccdVisitId
279 output_catalog["midPointTai"] = midPointTaiMJD
280 output_catalog["filterName"] = diff_exp.getFilterLabel().bandLabel
282 # Drop superfluous columns from output DataFrame.
283 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
285 return output_catalog
287 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure):
288 """Remove DiaForcedSources that are outside of the bounding box region.
290 Paramters
291 ---------
292 catalog : `pandas.DataFrame`
293 DiaForcedSources to check against the exposure bounding box.
294 updatedDiaObjectIds : `numpy.ndarray`
295 Array of diaObjectIds that were updated during this dia processing.
296 Used to assure that the pipeline includes all diaObjects that were
297 updated in case one falls on the edge of the CCD.
298 exposure : `lsst.afw.image.Exposure`
299 Exposure to check against.
301 Returns
302 -------
303 output : `pandas.DataFrame`
304 DataFrame trimmed to only the objects within the exposure bounding
305 box.
306 """
307 bbox = geom.Box2D(exposure.getBBox())
309 xS = catalog.loc[:, "x"]
310 yS = catalog.loc[:, "y"]
312 return catalog[
313 np.logical_or(bbox.contains(xS, yS),
314 np.isin(catalog.loc[:, "diaObjectId"],
315 updatedDiaObjectIds))]