Coverage for python/lsst/pipe/tasks/matchFakes.py: 43%
76 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:12 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:12 +0000
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/>.
22import astropy.units as u
23import numpy as np
24from scipy.spatial import cKDTree
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
32__all__ = ["MatchFakesTask",
33 "MatchFakesConfig",
34 "MatchVariableFakesConfig",
35 "MatchVariableFakesTask"]
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 )
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 )
89class MatchFakesTask(PipelineTask):
90 """Match a pre-existing catalog of fakes to a catalog of detections on
91 a difference image.
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 """
98 _DefaultName = "matchFakes"
99 ConfigClass = MatchFakesConfig
101 def runQuantum(self, butlerQC, inputRefs, outputRefs):
102 inputs = butlerQC.get(inputRefs)
104 outputs = self.run(**inputs)
105 butlerQC.put(outputs, outputRefs)
107 def run(self, fakeCat, diffIm, associatedDiaSources):
108 """Match fakes to detected diaSources within a difference image bound.
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``.
119 Returns
120 -------
121 result : `lsst.pipe.base.Struct`
122 Results struct with components.
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)
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"]))
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()
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))
146 return Struct(
147 matchedDiaSources=matchedFakes.merge(
148 associatedDiaSources.reset_index(drop=True), on="diaSourceId", how="left")
149 )
151 def _trimFakeCat(self, fakeCat, image):
152 """Trim the fake cat to about the size of the input image.
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
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()
168 bbox = Box2D(image.getBBox())
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)
177 return fakeCat[fakeCat.apply(trim, axis=1)]
179 def _getVectors(self, ras, decs):
180 """Convert ra dec to unit vectors on the sphere.
182 Parameters
183 ----------
184 ras : `numpy.ndarray`, (N,)
185 RA coordinates in radians.
186 decs : `numpy.ndarray`, (N,)
187 Dec coordinates in radians.
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))
196 vectors[:, 2] = np.sin(decs)
197 vectors[:, 0] = np.cos(decs) * np.cos(ras)
198 vectors[:, 1] = np.cos(decs) * np.sin(ras)
200 return vectors
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 )
212class MatchVariableFakesConfig(MatchFakesConfig,
213 pipelineConnections=MatchVariableFakesConnections):
214 """Config for MatchFakesTask.
215 """
216 pass
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.
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
231 def runQuantum(self, butlerQC, inputRefs, outputRefs):
232 inputs = butlerQC.get(inputRefs)
233 inputs["band"] = butlerQC.quantum.dataId["band"]
235 outputs = self.run(**inputs)
236 butlerQC.put(outputs, outputRefs)
238 def run(self, fakeCat, ccdVisitFakeMagnitudes, diffIm, associatedDiaSources, band):
239 """Match fakes to detected diaSources within a difference image bound.
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``.
250 Returns
251 -------
252 result : `lsst.pipe.base.Struct`
253 Results struct with components.
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)
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.
265 Negative magnitudes indicate that the source should be detected as
266 a negative source.
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))
285 noVisit = ~fakeCat["isVisitSource"]
286 noTemplate = ~fakeCat["isTemplateSource"]
287 both = np.logical_and(fakeCat["isVisitSource"],
288 fakeCat["isTemplateSource"])
290 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit]
291 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate]
292 fakeCat.loc[both, magName] = diffMag[both]