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',
120 'base_PsfFlux_chi2', 'slot_PsfFlux_chi2',
121 'base_PsfFlux_npixels', 'slot_PsfFlux_npixels',
122 ]
125class DiaForcedSourceTask(pipeBase.Task):
126 """Task for measuring and storing forced sources at DiaObject locations
127 in both difference and direct images.
128 """
129 ConfigClass = DiaForcedSourcedConfig
130 _DefaultName = "diaForcedSource"
132 def __init__(self, **kwargs):
133 pipeBase.Task.__init__(self, **kwargs)
134 self.makeSubtask("forcedMeasurement",
135 refSchema=afwTable.SourceTable.makeMinimalSchema())
137 @pipeBase.timeMethod
138 def run(self,
139 dia_objects,
140 updatedDiaObjectIds,
141 expIdBits,
142 exposure,
143 diffim):
144 """Measure forced sources on the direct and difference images.
146 Parameters
147 ----------
148 dia_objects : `pandas.DataFrame`
149 Catalog of previously observed and newly created DiaObjects
150 contained within the difference and direct images. DiaObjects
151 must be indexed on the ``diaObjectId`` column.
152 updatedDiaObjectIds : `numpy.ndarray`
153 Array of diaObjectIds that were updated during this dia processing.
154 Used to assure that the pipeline includes all diaObjects that were
155 updated in case one falls on the edge of the CCD.
156 expIdBits : `int`
157 Bit length of the exposure id.
158 exposure : `lsst.afw.image.Exposure`
159 Direct image exposure.
160 diffim : `lsst.afw.image.Exposure`
161 Difference image.
163 Returns
164 -------
165 output_forced_sources : `pandas.DataFrame`
166 Catalog of calibrated forced photometered fluxes on both the
167 difference and direct images at DiaObject locations.
168 """
170 afw_dia_objects = self._convert_from_pandas(dia_objects)
172 idFactoryDiff = afwTable.IdFactory.makeSource(
173 diffim.getInfo().getVisitInfo().getExposureId(),
174 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
176 diffForcedSources = self.forcedMeasurement.generateMeasCat(
177 diffim,
178 afw_dia_objects,
179 diffim.getWcs(),
180 idFactory=idFactoryDiff)
181 self.forcedMeasurement.run(
182 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
184 directForcedSources = self.forcedMeasurement.generateMeasCat(
185 exposure,
186 afw_dia_objects,
187 exposure.getWcs())
188 self.forcedMeasurement.run(
189 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
191 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
192 directForcedSources,
193 diffim,
194 exposure)
196 output_forced_sources = self._trim_to_exposure(output_forced_sources,
197 updatedDiaObjectIds,
198 exposure)
199 return output_forced_sources.set_index(
200 ["diaObjectId", "diaForcedSourceId"],
201 drop=False)
203 def _convert_from_pandas(self, input_objects):
204 """Create minimal schema SourceCatalog from a pandas DataFrame.
206 We need a catalog of this type to run within the forced measurement
207 subtask.
209 Parameters
210 ----------
211 input_objects : `pandas.DataFrame`
212 DiaObjects with locations and ids. ``
214 Returns
215 -------
216 outputCatalog : `lsst.afw.table.SourceTable`
217 Output catalog with minimal schema.
218 """
219 schema = afwTable.SourceTable.makeMinimalSchema()
221 outputCatalog = afwTable.SourceCatalog(schema)
222 outputCatalog.reserve(len(input_objects))
224 for obj_id, df_row in input_objects.iterrows():
225 outputRecord = outputCatalog.addNew()
226 outputRecord.setId(obj_id)
227 outputRecord.setCoord(
228 geom.SpherePoint(df_row["ra"],
229 df_row["decl"],
230 geom.degrees))
231 return outputCatalog
233 def _calibrate_and_merge(self,
234 diff_sources,
235 direct_sources,
236 diff_exp,
237 direct_exp):
238 """Take the two output catalogs from the ForcedMeasurementTasks and
239 calibrate, combine, and convert them to Pandas.
241 Parameters
242 ----------
243 diff_sources : `lsst.afw.table.SourceTable`
244 Catalog with PsFluxes measured on the difference image.
245 direct_sources : `lsst.afw.table.SourceTable`
246 Catalog with PsfFluxes measured on the direct (calexp) image.
247 diff_exp : `lsst.afw.image.Exposure`
248 Difference exposure ``diff_sources`` were measured on.
249 direct_exp : `lsst.afw.image.Exposure`
250 Direct (calexp) exposure ``direct_sources`` were measured on.
252 Returns
253 -------
254 output_catalog : `pandas.DataFrame`
255 Catalog calibrated diaForcedSources.
256 """
257 diff_calib = diff_exp.getPhotoCalib()
258 direct_calib = direct_exp.getPhotoCalib()
260 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
261 "slot_PsfFlux")
262 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
263 "slot_PsfFlux")
265 output_catalog = diff_sources.asAstropy().to_pandas()
266 output_catalog.rename(columns={"id": "diaForcedSourceId",
267 "slot_PsfFlux_instFlux": "psFlux",
268 "slot_PsfFlux_instFluxErr": "psFluxErr",
269 "slot_Centroid_x": "x",
270 "slot_Centroid_y": "y"},
271 inplace=True)
272 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
273 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
275 output_catalog["totFlux"] = direct_fluxes[:, 0]
276 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
278 visit_info = direct_exp.getInfo().getVisitInfo()
279 ccdVisitId = visit_info.getExposureId()
280 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
281 output_catalog["ccdVisitId"] = ccdVisitId
282 output_catalog["midPointTai"] = midPointTaiMJD
283 output_catalog["filterName"] = diff_exp.getFilterLabel().bandLabel
285 # Drop superfluous columns from output DataFrame.
286 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
288 return output_catalog
290 def _trim_to_exposure(self, catalog, updatedDiaObjectIds, exposure):
291 """Remove DiaForcedSources that are outside of the bounding box region.
293 Paramters
294 ---------
295 catalog : `pandas.DataFrame`
296 DiaForcedSources to check against the exposure bounding box.
297 updatedDiaObjectIds : `numpy.ndarray`
298 Array of diaObjectIds that were updated during this dia processing.
299 Used to assure that the pipeline includes all diaObjects that were
300 updated in case one falls on the edge of the CCD.
301 exposure : `lsst.afw.image.Exposure`
302 Exposure to check against.
304 Returns
305 -------
306 output : `pandas.DataFrame`
307 DataFrame trimmed to only the objects within the exposure bounding
308 box.
309 """
310 bbox = geom.Box2D(exposure.getBBox())
312 xS = catalog.loc[:, "x"]
313 yS = catalog.loc[:, "y"]
315 return catalog[
316 np.logical_or(bbox.contains(xS, yS),
317 np.isin(catalog.loc[:, "diaObjectId"],
318 updatedDiaObjectIds))]