lsst.pipe.tasks 21.0.0-178-g80f1dd77+d3f2e76fa8
matchFakes.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
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/>.
21
22import astropy.units as u
23import numpy as np
24from scipy.spatial import cKDTree
25
26from lsst.geom import Box2D, radians, SpherePoint
27import lsst.pex.config as pexConfig
28from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct
29import lsst.pipe.base.connectionTypes as connTypes
30from lsst.pipe.tasks.insertFakes import InsertFakesConfig
31
32__all__ = ["MatchFakesTask",
33 "MatchFakesConfig",
34 "MatchVariableFakesConfig",
35 "MatchVariableFakesTask"]
36
37
38class MatchFakesConnections(PipelineTaskConnections,
39 defaultTemplates={"coaddName": "deep",
40 "fakesType": "fakes_"},
41 dimensions=("tract",
42 "skymap",
43 "instrument",
44 "visit",
45 "detector")):
46 fakeCat = connTypes.Input(
47 doc="Catalog of fake sources inserted into an image.",
48 name="{fakesType}fakeSourceCat",
49 storageClass="DataFrame",
50 dimensions=("tract", "skymap")
51 )
52 diffIm = connTypes.Input(
53 doc="Difference image on which the DiaSources were detected.",
54 name="{fakesType}{coaddName}Diff_differenceExp",
55 storageClass="ExposureF",
56 dimensions=("instrument", "visit", "detector"),
57 )
58 associatedDiaSources = connTypes.Input(
59 doc="A DiaSource catalog to match against fakeCat. Assumed "
60 "to be SDMified.",
61 name="{fakesType}{coaddName}Diff_assocDiaSrc",
62 storageClass="DataFrame",
63 dimensions=("instrument", "visit", "detector"),
64 )
65 matchedDiaSources = connTypes.Output(
66 doc="A catalog of those fakeCat sources that have a match in "
67 "associatedDiaSources. The schema is the union of the schemas for "
68 "``fakeCat`` and ``associatedDiaSources``.",
69 name="{fakesType}{coaddName}Diff_matchDiaSrc",
70 storageClass="DataFrame",
71 dimensions=("instrument", "visit", "detector"),
72 )
73
74
75class MatchFakesConfig(
76 InsertFakesConfig,
77 pipelineConnections=MatchFakesConnections):
78 """Config for MatchFakesTask.
79 """
80 matchDistanceArcseconds = pexConfig.RangeField(
81 doc="Distance in arcseconds to match within.",
82 dtype=float,
83 default=0.5,
84 min=0,
85 max=10,
86 )
87
88
89class MatchFakesTask(PipelineTask):
90 """Match a pre-existing catalog of fakes to a catalog of detections on
91 a difference image.
92
93 This task is generally for injected sources that cannot be easily
94 identified by their footprints such as in the case of detector sources
95 post image differencing.
96 """
97
98 _DefaultName = "matchFakes"
99 ConfigClass = MatchFakesConfig
100
101 def runQuantum(self, butlerQC, inputRefs, outputRefs):
102 inputs = butlerQC.get(inputRefs)
103
104 outputs = self.run(**inputs)
105 butlerQC.put(outputs, outputRefs)
106
107 def run(self, fakeCat, diffIm, associatedDiaSources):
108 """Match fakes to detected diaSources within a difference image bound.
109
110 Parameters
111 ----------
112 fakeCat : `pandas.DataFrame`
113 Catalog of fakes to match to detected diaSources.
114 diffIm : `lsst.afw.image.Exposure`
115 Difference image where ``associatedDiaSources`` were detected.
116 associatedDiaSources : `pandas.DataFrame`
117 Catalog of difference image sources detected in ``diffIm``.
118
119 Returns
120 -------
121 result : `lsst.pipe.base.Struct`
122 Results struct with components.
123
124 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
125 length of ``fakeCat``. (`pandas.DataFrame`)
126 """
127 trimmedFakes = self._trimFakeCat(fakeCat, diffIm)
128 nPossibleFakes = len(trimmedFakes)
129
130 fakeVects = self._getVectors(trimmedFakes[self.config.raColName],
131 trimmedFakes[self.config.decColName])
132 diaSrcVects = self._getVectors(
133 np.radians(associatedDiaSources.loc[:, "ra"]),
134 np.radians(associatedDiaSources.loc[:, "decl"]))
135
136 diaSrcTree = cKDTree(diaSrcVects)
137 dist, idxs = diaSrcTree.query(
138 fakeVects,
139 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
140 nFakesFound = np.isfinite(dist).sum()
141
142 self.log.info("Found %d out of %d possible.", nFakesFound, nPossibleFakes)
143 diaSrcIds = associatedDiaSources.iloc[np.where(np.isfinite(dist), idxs, 0)]["diaSourceId"].to_numpy()
144 matchedFakes = trimmedFakes.assign(diaSourceId=np.where(np.isfinite(dist), diaSrcIds, 0))
145
146 return Struct(
147 matchedDiaSources=matchedFakes.merge(
148 associatedDiaSources.reset_index(drop=True), on="diaSourceId", how="left")
149 )
150
151 def _trimFakeCat(self, fakeCat, image):
152 """Trim the fake cat to about the size of the input image.
153
154 Parameters
155 ----------
156 fakeCat : `pandas.core.frame.DataFrame`
157 The catalog of fake sources to be input
158 image : `lsst.afw.image.exposure.exposure.ExposureF`
159 The image into which the fake sources should be added
160
161 Returns
162 -------
163 fakeCat : `pandas.core.frame.DataFrame`
164 The original fakeCat trimmed to the area of the image
165 """
166 wcs = image.getWcs()
167
168 bbox = Box2D(image.getBBox())
169
170 def trim(row):
171 coord = SpherePoint(row[self.config.raColName],
172 row[self.config.decColName],
173 radians)
174 cent = wcs.skyToPixel(coord)
175 return bbox.contains(cent)
176
177 return fakeCat[fakeCat.apply(trim, axis=1)]
178
179 def _getVectors(self, ras, decs):
180 """Convert ra dec to unit vectors on the sphere.
181
182 Parameters
183 ----------
184 ras : `numpy.ndarray`, (N,)
185 RA coordinates in radians.
186 decs : `numpy.ndarray`, (N,)
187 Dec coordinates in radians.
188
189 Returns
190 -------
191 vectors : `numpy.ndarray`, (N, 3)
192 Vectors on the unit sphere for the given RA/DEC values.
193 """
194 vectors = np.empty((len(ras), 3))
195
196 vectors[:, 2] = np.sin(decs)
197 vectors[:, 0] = np.cos(decs) * np.cos(ras)
198 vectors[:, 1] = np.cos(decs) * np.sin(ras)
199
200 return vectors
201
202
203class MatchVariableFakesConnections(MatchFakesConnections):
204 ccdVisitFakeMagnitudes = connTypes.Input(
205 doc="Catalog of fakes with magnitudes scattered for this ccdVisit.",
206 name="{fakesType}ccdVisitFakeMagnitudes",
207 storageClass="DataFrame",
208 dimensions=("instrument", "visit", "detector"),
209 )
210
211
212class MatchVariableFakesConfig(MatchFakesConfig,
213 pipelineConnections=MatchVariableFakesConnections):
214 """Config for MatchFakesTask.
215 """
216 pass
217
218
219class MatchVariableFakesTask(MatchFakesTask):
220 """Match injected fakes to their detected sources in the catalog and
221 compute their expected brightness in a difference image assuming perfect
222 subtraction.
223
224 This task is generally for injected sources that cannot be easily
225 identified by their footprints such as in the case of detector sources
226 post image differencing.
227 """
228 _DefaultName = "matchVariableFakes"
229 ConfigClass = MatchVariableFakesConfig
230
231 def runQuantum(self, butlerQC, inputRefs, outputRefs):
232 inputs = butlerQC.get(inputRefs)
233 inputs["band"] = butlerQC.quantum.dataId["band"]
234
235 outputs = self.run(**inputs)
236 butlerQC.put(outputs, outputRefs)
237
238 def run(self, fakeCat, ccdVisitFakeMagnitudes, diffIm, associatedDiaSources, band):
239 """Match fakes to detected diaSources within a difference image bound.
240
241 Parameters
242 ----------
243 fakeCat : `pandas.DataFrame`
244 Catalog of fakes to match to detected diaSources.
245 diffIm : `lsst.afw.image.Exposure`
246 Difference image where ``associatedDiaSources`` were detected in.
247 associatedDiaSources : `pandas.DataFrame`
248 Catalog of difference image sources detected in ``diffIm``.
249
250 Returns
251 -------
252 result : `lsst.pipe.base.Struct`
253 Results struct with components.
254
255 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
256 length of ``fakeCat``. (`pandas.DataFrame`)
257 """
258 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band)
259 return super().run(fakeCat, diffIm, associatedDiaSources)
260
261 def computeExpectedDiffMag(self, fakeCat, ccdVisitFakeMagnitudes, band):
262 """Compute the magnitude expected in the difference image for this
263 detector/visit. Modify fakeCat in place.
264
265 Negative magnitudes indicate that the source should be detected as
266 a negative source.
267
268 Parameters
269 ----------
270 fakeCat : `pandas.DataFrame`
271 Catalog of fake sources.
272 ccdVisitFakeMagnitudes : `pandas.DataFrame`
273 Magnitudes for variable sources in this specific ccdVisit.
274 band : `str`
275 Band that this ccdVisit was observed in.
276 """
277 magName = self.config.mag_col % band
278 magnitudes = fakeCat[magName].to_numpy()
279 visitMags = ccdVisitFakeMagnitudes["variableMag"].to_numpy()
280 diffFlux = (visitMags * u.ABmag).to_value(u.nJy) - (magnitudes * u.ABmag).to_value(u.nJy)
281 diffMag = np.where(diffFlux > 0,
282 (diffFlux * u.nJy).to_value(u.ABmag),
283 -(-diffFlux * u.nJy).to_value(u.ABmag))
284
285 noVisit = ~fakeCat["isVisitSource"]
286 noTemplate = ~fakeCat["isTemplateSource"]
287 both = np.logical_and(fakeCat["isVisitSource"],
288 fakeCat["isTemplateSource"])
289
290 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit]
291 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate]
292 fakeCat.loc[both, magName] = diffMag[both]