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

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 lsst.afw.table as afwTable
29from lsst.daf.base import DateTime
30import lsst.geom as geom
31from lsst.meas.base.pluginRegistry import register
32from lsst.meas.base import (
33 ForcedMeasurementTask,
34 ForcedTransformedCentroidConfig,
35 ForcedTransformedCentroidPlugin)
36import lsst.pex.config as pexConfig
37import lsst.pipe.base as pipeBase
40class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig):
41 """Configuration for the forced transformed coord algorithm.
42 """
43 pass
46@register("ap_assoc_TransformedCentroid")
47class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin):
48 """Record the transformation of the reference catalog coord.
49 The coord recorded in the reference catalog is tranformed to the
50 measurement coordinate system and stored.
52 Parameters
53 ----------
54 config : `ForcedTransformedCentroidFromCoordConfig`
55 Plugin configuration
56 name : `str`
57 Plugin name
58 schemaMapper : `lsst.afw.table.SchemaMapper`
59 A mapping from reference catalog fields to output
60 catalog fields. Output fields are added to the output schema.
61 metadata : `lsst.daf.base.PropertySet`
62 Plugin metadata that will be attached to the output catalog.
64 Notes
65 -----
66 This can be used as the slot centroid in forced measurement when only a
67 reference coord exits, allowing subsequent measurements to simply refer to
68 the slot value just as they would in single-frame measurement.
69 """
71 ConfigClass = ForcedTransformedCentroidFromCoordConfig
73 def measure(self, measRecord, exposure, refRecord, refWcs):
74 targetWcs = exposure.getWcs()
76 targetPos = targetWcs.skyToPixel(refRecord.getCoord())
77 measRecord.set(self.centroidKey, targetPos)
79 if self.flagKey is not None:
80 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
83class DiaForcedSourcedConfig(pexConfig.Config):
84 """Configuration for the generic DiaForcedSourcedTask class.
85 """
86 forcedMeasurement = pexConfig.ConfigurableField(
87 target=ForcedMeasurementTask,
88 doc="Subtask to force photometer DiaObjects in the direct and "
89 "difference images.",
90 )
91 dropColumns = pexConfig.ListField(
92 dtype=str,
93 doc="Columns produced in forced measurement that can be dropped upon "
94 "creation and storage of the final pandas data.",
95 )
97 def setDefaults(self):
98 self.forcedMeasurement.plugins = ["ap_assoc_TransformedCentroid",
99 "base_PsfFlux"]
100 self.forcedMeasurement.doReplaceWithNoise = False
101 self.forcedMeasurement.copyColumns = {
102 "id": "diaObjectId",
103 "coord_ra": "coord_ra",
104 "coord_dec": "coord_dec"}
105 self.forcedMeasurement.slots.centroid = "ap_assoc_TransformedCentroid"
106 self.forcedMeasurement.slots.psfFlux = "base_PsfFlux"
107 self.forcedMeasurement.slots.shape = None
108 self.dropColumns = ['coord_ra', 'coord_dec', 'parent',
109 'ap_assoc_TransformedCentroid_x',
110 'ap_assoc_TransformedCentroid_y',
111 'base_PsfFlux_instFlux',
112 'base_PsfFlux_instFluxErr', 'base_PsfFlux_area',
113 'slot_PsfFlux_area', 'base_PsfFlux_flag',
114 'slot_PsfFlux_flag',
115 'base_PsfFlux_flag_noGoodPixels',
116 'slot_PsfFlux_flag_noGoodPixels',
117 'base_PsfFlux_flag_edge', 'slot_PsfFlux_flag_edge']
120class DiaForcedSourceTask(pipeBase.Task):
121 """Task for measuring and storing forced sources at DiaObject locations
122 in both difference and direct images.
123 """
124 ConfigClass = DiaForcedSourcedConfig
125 _DefaultName = "diaForcedSource"
127 def __init__(self, **kwargs):
128 pipeBase.Task.__init__(self, **kwargs)
129 self.makeSubtask("forcedMeasurement",
130 refSchema=afwTable.SourceTable.makeMinimalSchema())
132 @pipeBase.timeMethod
133 def run(self, dia_objects, expIdBits, exposure, diffim):
134 """Measure forced sources on the direct and difference images.
136 Parameters
137 ----------
138 dia_objects : `pandas.DataFrame`
139 Catalog of previously observed and newly created DiaObjects
140 contained within the difference and direct images.
141 expIdBits : `int`
142 Bit length of the exposure id.
143 exposure : `lsst.afw.image.Exposure`
144 Direct image exposure.
145 diffim : `lsst.afw.image.Exposure`
146 Difference image.
148 Returns
149 -------
150 output_forced_sources : `pandas.DataFrame`
151 Catalog of calibrated forced photometered fluxes on both the
152 difference and direct images at DiaObject locations.
153 """
155 afw_dia_objects = self._convert_from_pandas(dia_objects)
157 idFactoryDiff = afwTable.IdFactory.makeSource(
158 diffim.getInfo().getVisitInfo().getExposureId(),
159 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
161 diffForcedSources = self.forcedMeasurement.generateMeasCat(
162 diffim,
163 afw_dia_objects,
164 diffim.getWcs(),
165 idFactory=idFactoryDiff)
166 self.forcedMeasurement.run(
167 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
169 directForcedSources = self.forcedMeasurement.generateMeasCat(
170 exposure,
171 afw_dia_objects,
172 exposure.getWcs())
173 self.forcedMeasurement.run(
174 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
176 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
177 directForcedSources,
178 diffim,
179 exposure)
181 output_forced_sources = self._trim_to_exposure(output_forced_sources,
182 exposure)
183 return output_forced_sources.set_index(
184 ["diaObjectId", "diaForcedSourceId"],
185 drop=False)
187 def _convert_from_pandas(self, input_objects):
188 """Create minimal schema SourceCatalog from a pandas DataFrame.
190 We need a catalog of this type to run within the forced measurement
191 subtask.
193 Parameters
194 ----------
195 input_objects : `pandas.DataFrame`
196 DiaObjects with locations and ids. ``
198 Returns
199 -------
200 outputCatalog : `lsst.afw.table.SourceTable`
201 Output catalog with minimal schema.
202 """
203 schema = afwTable.SourceTable.makeMinimalSchema()
205 outputCatalog = afwTable.SourceCatalog(schema)
206 outputCatalog.reserve(len(input_objects))
208 for obj_id, df_row in input_objects.iterrows():
209 outputRecord = outputCatalog.addNew()
210 outputRecord.setId(obj_id)
211 outputRecord.setCoord(
212 geom.SpherePoint(df_row["ra"],
213 df_row["decl"],
214 geom.degrees))
215 return outputCatalog
217 def _calibrate_and_merge(self,
218 diff_sources,
219 direct_sources,
220 diff_exp,
221 direct_exp):
222 """Take the two output catalogs from the ForcedMeasurementTasks and
223 calibrate, combine, and convert them to Pandas.
225 Parameters
226 ----------
227 diff_sources : `lsst.afw.table.SourceTable`
228 Catalog with PsFluxes measured on the difference image.
229 direct_sources : `lsst.afw.table.SourceTable`
230 Catalog with PsfFluxes measured on the direct (calexp) image.
231 diff_exp : `lsst.afw.image.Exposure`
232 Difference exposure ``diff_sources`` were measured on.
233 direct_exp : `lsst.afw.image.Exposure`
234 Direct (calexp) exposure ``direct_sources`` were measured on.
236 Returns
237 -------
238 output_catalog : `pandas.DataFrame`
239 Catalog calibrated diaForcedSources.
240 """
241 diff_calib = diff_exp.getPhotoCalib()
242 direct_calib = direct_exp.getPhotoCalib()
244 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
245 "slot_PsfFlux")
246 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
247 "slot_PsfFlux")
249 output_catalog = diff_sources.asAstropy().to_pandas()
250 output_catalog.rename(columns={"id": "diaForcedSourceId",
251 "slot_PsfFlux_instFlux": "psFlux",
252 "slot_PsfFlux_instFluxErr": "psFluxErr",
253 "slot_Centroid_x": "x",
254 "slot_Centroid_y": "y"},
255 inplace=True)
256 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
257 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
259 output_catalog["totFlux"] = direct_fluxes[:, 0]
260 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
262 visit_info = direct_exp.getInfo().getVisitInfo()
263 ccdVisitId = visit_info.getExposureId()
264 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
265 output_catalog["ccdVisitId"] = ccdVisitId
266 output_catalog["midPointTai"] = midPointTaiMJD
267 output_catalog["filterName"] = diff_exp.getFilter().getCanonicalName()
269 # Drop superfluous columns from output DataFrame.
270 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
272 return output_catalog
274 def _trim_to_exposure(self, catalog, exposure):
275 """Remove DiaForcedSources that are outside of the bounding box region.
277 Paramters
278 ---------
279 catalog : `pandas.DataFrame`
280 DiaForcedSources to check against the exposure bounding box.
281 exposure : `lsst.afw.image.Exposure`
282 Exposure to check against.
284 Returns
285 -------
286 output : `pandas.DataFrame`
287 DataFrame trimmed to only the objects within the exposure bounding
288 box.
289 """
290 bbox = geom.Box2D(exposure.getBBox())
292 xS = catalog.loc[:, "x"]
293 yS = catalog.loc[:, "y"]
295 return catalog[bbox.contains(xS, yS)]