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 def run(self, dia_objects, expIdBits, exposure, diffim):
133 """Measure forced sources on the direct and difference images.
135 Parameters
136 ----------
137 dia_objects : `pandas.DataFrame`
138 Catalog of previously observed and newly created DiaObjects
139 contained within the difference and direct images.
140 expIdBits : `int`
141 Bit length of the exposure id.
142 exposure : `lsst.afw.image.Exposure`
143 Direct image exposure.
144 diffim : `lsst.afw.image.Exposure`
145 Difference image.
147 Returns
148 -------
149 output_forced_sources : `pandas.DataFrame`
150 Catalog of calibrated forced photometered fluxes on both the
151 difference and direct images at DiaObject locations.
152 """
154 afw_dia_objects = self._convert_from_pandas(dia_objects)
156 idFactoryDiff = afwTable.IdFactory.makeSource(
157 diffim.getInfo().getVisitInfo().getExposureId(),
158 afwTable.IdFactory.computeReservedFromMaxBits(int(expIdBits)))
160 diffForcedSources = self.forcedMeasurement.generateMeasCat(
161 diffim,
162 afw_dia_objects,
163 diffim.getWcs(),
164 idFactory=idFactoryDiff)
165 self.forcedMeasurement.run(
166 diffForcedSources, diffim, afw_dia_objects, diffim.getWcs())
168 directForcedSources = self.forcedMeasurement.generateMeasCat(
169 exposure,
170 afw_dia_objects,
171 exposure.getWcs())
172 self.forcedMeasurement.run(
173 directForcedSources, exposure, afw_dia_objects, exposure.getWcs())
175 output_forced_sources = self._calibrate_and_merge(diffForcedSources,
176 directForcedSources,
177 diffim,
178 exposure)
180 output_forced_sources = self._trim_to_exposure(output_forced_sources,
181 exposure)
182 return output_forced_sources.set_index(
183 ["diaObjectId", "diaForcedSourceId"],
184 drop=False)
186 def _convert_from_pandas(self, input_objects):
187 """Create minimal schema SourceCatalog from a pandas DataFrame.
189 We need a catalog of this type to run within the forced measurement
190 subtask.
192 Parameters
193 ----------
194 input_objects : `pandas.DataFrame`
195 DiaObjects with locations and ids. ``
197 Returns
198 -------
199 outputCatalog : `lsst.afw.table.SourceTable`
200 Output catalog with minimal schema.
201 """
202 schema = afwTable.SourceTable.makeMinimalSchema()
204 outputCatalog = afwTable.SourceCatalog(schema)
205 outputCatalog.reserve(len(input_objects))
207 for obj_id, df_row in input_objects.iterrows():
208 outputRecord = outputCatalog.addNew()
209 outputRecord.setId(obj_id)
210 outputRecord.setCoord(
211 geom.SpherePoint(df_row["ra"],
212 df_row["decl"],
213 geom.degrees))
214 return outputCatalog
216 def _calibrate_and_merge(self,
217 diff_sources,
218 direct_sources,
219 diff_exp,
220 direct_exp):
221 """Take the two output catalogs from the ForcedMeasurementTasks and
222 calibrate, combine, and convert them to Pandas.
224 Parameters
225 ----------
226 diff_sources : `lsst.afw.table.SourceTable`
227 Catalog with PsFluxes measured on the difference image.
228 direct_sources : `lsst.afw.table.SourceTable`
229 Catalog with PsfFluxes measured on the direct (calexp) image.
230 diff_exp : `lsst.afw.image.Exposure`
231 Difference exposure ``diff_sources`` were measured on.
232 direct_exp : `lsst.afw.image.Exposure`
233 Direct (calexp) exposure ``direct_sources`` were measured on.
235 Returns
236 -------
237 output_catalog : `pandas.DataFrame`
238 Catalog calibrated diaForcedSources.
239 """
240 diff_calib = diff_exp.getPhotoCalib()
241 direct_calib = direct_exp.getPhotoCalib()
243 diff_fluxes = diff_calib.instFluxToNanojansky(diff_sources,
244 "slot_PsfFlux")
245 direct_fluxes = direct_calib.instFluxToNanojansky(direct_sources,
246 "slot_PsfFlux")
248 output_catalog = diff_sources.asAstropy().to_pandas()
249 output_catalog.rename(columns={"id": "diaForcedSourceId",
250 "slot_PsfFlux_instFlux": "psFlux",
251 "slot_PsfFlux_instFluxErr": "psFluxErr",
252 "slot_Centroid_x": "x",
253 "slot_Centroid_y": "y"},
254 inplace=True)
255 output_catalog.loc[:, "psFlux"] = diff_fluxes[:, 0]
256 output_catalog.loc[:, "psFluxErr"] = diff_fluxes[:, 1]
258 output_catalog["totFlux"] = direct_fluxes[:, 0]
259 output_catalog["totFluxErr"] = direct_fluxes[:, 1]
261 visit_info = direct_exp.getInfo().getVisitInfo()
262 ccdVisitId = visit_info.getExposureId()
263 midPointTaiMJD = visit_info.getDate().get(system=DateTime.MJD)
264 output_catalog["ccdVisitId"] = ccdVisitId
265 output_catalog["midPointTai"] = midPointTaiMJD
266 output_catalog["filterName"] = diff_exp.getFilter().getCanonicalName()
268 # Drop superfluous columns from output DataFrame.
269 output_catalog.drop(columns=self.config.dropColumns, inplace=True)
271 return output_catalog
273 def _trim_to_exposure(self, catalog, exposure):
274 """Remove DiaForcedSources that are outside of the bounding box region.
276 Paramters
277 ---------
278 catalog : `pandas.DataFrame`
279 DiaForcedSources to check against the exposure bounding box.
280 exposure : `lsst.afw.image.Exposure`
281 Exposure to check against.
283 Returns
284 -------
285 output : `pandas.DataFrame`
286 DataFrame trimmed to only the objects within the exposure bounding
287 box.
288 """
289 bbox = geom.Box2D(exposure.getBBox())
291 xS = catalog.loc[:, "x"]
292 yS = catalog.loc[:, "y"]
294 return catalog[bbox.contains(xS, yS)]