LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
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
22__all__ = ["MatchFakesTask",
23 "MatchFakesConfig",
24 "MatchVariableFakesConfig",
25 "MatchVariableFakesTask"]
26
27import astropy.units as u
28import numpy as np
29import pandas as pd
30from scipy.spatial import cKDTree
31
32from lsst.geom import Box2D
33import lsst.pex.config as pexConfig
34from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct
35import lsst.pipe.base.connectionTypes as connTypes
36from lsst.skymap import BaseSkyMap
37
38from lsst.pipe.tasks.insertFakes import InsertFakesConfig
39
40from deprecated.sphinx import deprecated
41
42
43class MatchFakesConnections(PipelineTaskConnections,
44 defaultTemplates={"coaddName": "deep",
45 "fakesType": "fakes_"},
46 dimensions=("instrument",
47 "visit",
48 "detector")):
49 skyMap = connTypes.Input(
50 doc="Input definition of geometry/bbox and projection/wcs for "
51 "template exposures. Needed to test which tract to generate ",
52 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
53 dimensions=("skymap",),
54 storageClass="SkyMap",
55 )
56 fakeCats = connTypes.Input(
57 doc="Catalog of fake sources inserted into an image.",
58 name="{fakesType}fakeSourceCat",
59 storageClass="DataFrame",
60 dimensions=("tract", "skymap"),
61 deferLoad=True,
62 multiple=True
63 )
64 diffIm = connTypes.Input(
65 doc="Difference image on which the DiaSources were detected.",
66 name="{fakesType}{coaddName}Diff_differenceExp",
67 storageClass="ExposureF",
68 dimensions=("instrument", "visit", "detector"),
69 )
70 associatedDiaSources = connTypes.Input(
71 doc="A DiaSource catalog to match against fakeCat. Assumed "
72 "to be SDMified.",
73 name="{fakesType}{coaddName}Diff_assocDiaSrc",
74 storageClass="DataFrame",
75 dimensions=("instrument", "visit", "detector"),
76 )
77 matchedDiaSources = connTypes.Output(
78 doc="A catalog of those fakeCat sources that have a match in "
79 "associatedDiaSources. The schema is the union of the schemas for "
80 "``fakeCat`` and ``associatedDiaSources``.",
81 name="{fakesType}{coaddName}Diff_matchDiaSrc",
82 storageClass="DataFrame",
83 dimensions=("instrument", "visit", "detector"),
84 )
85
86
87@deprecated(
88 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
89 version="v28.0",
90 category=FutureWarning,
91)
92class MatchFakesConfig(
93 InsertFakesConfig,
94 pipelineConnections=MatchFakesConnections):
95 """Config for MatchFakesTask.
96 """
97 matchDistanceArcseconds = pexConfig.RangeField(
98 doc="Distance in arcseconds to match within.",
99 dtype=float,
100 default=0.5,
101 min=0,
102 max=10,
103 )
104
105 doMatchVisit = pexConfig.Field(
106 dtype=bool,
107 default=False,
108 doc="Match visit to trim the fakeCat"
109 )
110
111 trimBuffer = pexConfig.Field(
112 doc="Size of the pixel buffer surrounding the image. Only those fake sources with a centroid"
113 "falling within the image+buffer region will be considered matches.",
114 dtype=int,
115 default=100,
116 )
117
118
119@deprecated(
120 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
121 version="v28.0",
122 category=FutureWarning,
123)
124class MatchFakesTask(PipelineTask):
125 """Match a pre-existing catalog of fakes to a catalog of detections on
126 a difference image.
127
128 This task is generally for injected sources that cannot be easily
129 identified by their footprints such as in the case of detector sources
130 post image differencing.
131 """
132
133 _DefaultName = "matchFakes"
134 ConfigClass = MatchFakesConfig
135
136 def run(self, fakeCats, skyMap, diffIm, associatedDiaSources):
137 """Compose fakes into a single catalog and match fakes to detected
138 diaSources within a difference image bound.
139
140 Parameters
141 ----------
142 fakeCats : `pandas.DataFrame`
143 List of catalog of fakes to match to detected diaSources.
144 skyMap : `lsst.skymap.SkyMap`
145 SkyMap defining the tracts and patches the fakes are stored over.
146 diffIm : `lsst.afw.image.Exposure`
147 Difference image where ``associatedDiaSources`` were detected.
148 associatedDiaSources : `pandas.DataFrame`
149 Catalog of difference image sources detected in ``diffIm``.
150
151 Returns
152 -------
153 result : `lsst.pipe.base.Struct`
154 Results struct with components.
155
156 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
157 length of ``fakeCat``. (`pandas.DataFrame`)
158 """
159 fakeCat = self.composeFakeCat(fakeCats, skyMap)
160
161 if self.config.doMatchVisit:
162 fakeCat = self.getVisitMatchedFakeCat(fakeCat, diffIm)
163
164 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
165
166 def _processFakes(self, fakeCat, diffIm, associatedDiaSources):
167 """Match fakes to detected diaSources within a difference image bound.
168
169 Parameters
170 ----------
171 fakeCat : `pandas.DataFrame`
172 Catalog of fakes to match to detected diaSources.
173 diffIm : `lsst.afw.image.Exposure`
174 Difference image where ``associatedDiaSources`` were detected.
175 associatedDiaSources : `pandas.DataFrame`
176 Catalog of difference image sources detected in ``diffIm``.
177
178 Returns
179 -------
180 result : `lsst.pipe.base.Struct`
181 Results struct with components.
182
183 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
184 length of ``fakeCat``. (`pandas.DataFrame`)
185 """
186 trimmedFakes = self._trimFakeCat(fakeCat, diffIm)
187 nPossibleFakes = len(trimmedFakes)
188
189 fakeVects = self._getVectors(trimmedFakes[self.config.ra_col],
190 trimmedFakes[self.config.dec_col])
191 diaSrcVects = self._getVectors(
192 np.radians(associatedDiaSources.loc[:, "ra"]),
193 np.radians(associatedDiaSources.loc[:, "dec"]))
194
195 diaSrcTree = cKDTree(diaSrcVects)
196 dist, idxs = diaSrcTree.query(
197 fakeVects,
198 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
199 nFakesFound = np.isfinite(dist).sum()
200
201 self.log.info("Found %d out of %d possible.", nFakesFound, nPossibleFakes)
202 diaSrcIds = associatedDiaSources.iloc[np.where(np.isfinite(dist), idxs, 0)]["diaSourceId"].to_numpy()
203 matchedFakes = trimmedFakes.assign(diaSourceId=np.where(np.isfinite(dist), diaSrcIds, 0))
204
205 return Struct(
206 matchedDiaSources=matchedFakes.merge(
207 associatedDiaSources.reset_index(drop=True), on="diaSourceId", how="left")
208 )
209
210 def composeFakeCat(self, fakeCats, skyMap):
211 """Concatenate the fakeCats from tracts that may cover the exposure.
212
213 Parameters
214 ----------
215 fakeCats : `list` of `lsst.daf.butler.DeferredDatasetHandle`
216 Set of fake cats to concatenate.
217 skyMap : `lsst.skymap.SkyMap`
218 SkyMap defining the geometry of the tracts and patches.
219
220 Returns
221 -------
222 combinedFakeCat : `pandas.DataFrame`
223 All fakes that cover the inner polygon of the tracts in this
224 quantum.
225 """
226 if len(fakeCats) == 1:
227 return fakeCats[0].get()
228 outputCat = []
229 for fakeCatRef in fakeCats:
230 cat = fakeCatRef.get()
231 tractId = fakeCatRef.dataId["tract"]
232 # Make sure all data is within the inner part of the tract.
233 outputCat.append(cat[
234 skyMap.findTractIdArray(cat[self.config.ra_col],
235 cat[self.config.dec_col],
236 degrees=False)
237 == tractId])
238
239 return pd.concat(outputCat)
240
241 def getVisitMatchedFakeCat(self, fakeCat, exposure):
242 """Trim the fakeCat to select particular visit
243
244 Parameters
245 ----------
246 fakeCat : `pandas.core.frame.DataFrame`
247 The catalog of fake sources to add to the exposure
248 exposure : `lsst.afw.image.exposure.exposure.ExposureF`
249 The exposure to add the fake sources to
250
251 Returns
252 -------
253 movingFakeCat : `pandas.DataFrame`
254 All fakes that belong to the visit
255 """
256 selected = exposure.getInfo().getVisitInfo().getId() == fakeCat["visit"]
257
258 return fakeCat[selected]
259
260 def _addPixCoords(self, fakeCat, image):
261
262 """Add pixel coordinates to the catalog of fakes.
263
264 Parameters
265 ----------
266 fakeCat : `pandas.core.frame.DataFrame`
267 The catalog of fake sources to be input
268 image : `lsst.afw.image.exposure.exposure.ExposureF`
269 The image into which the fake sources should be added
270 Returns
271 -------
272 fakeCat : `pandas.core.frame.DataFrame`
273 """
274 wcs = image.getWcs()
275 ras = fakeCat[self.config.ra_col].values
276 decs = fakeCat[self.config.dec_col].values
277 xs, ys = wcs.skyToPixelArray(ras, decs)
278 fakeCat["x"] = xs
279 fakeCat["y"] = ys
280
281 return fakeCat
282
283 def _trimFakeCat(self, fakeCat, image):
284 """Trim the fake cat to the exact size of the input image.
285
286 Parameters
287 ----------
288 fakeCat : `pandas.core.frame.DataFrame`
289 The catalog of fake sources that was input
290 image : `lsst.afw.image.exposure.exposure.ExposureF`
291 The image into which the fake sources were added
292 Returns
293 -------
294 fakeCat : `pandas.core.frame.DataFrame`
295 The original fakeCat trimmed to the area of the image
296 """
297
298 # fakeCat must be processed with _addPixCoords before trimming
299 if ('x' not in fakeCat.columns) or ('y' not in fakeCat.columns):
300 fakeCat = self._addPixCoords(fakeCat, image)
301
302 # Prefilter in ra/dec to avoid cases where the wcs incorrectly maps
303 # input fakes which are really off the chip onto it.
304 ras = fakeCat[self.config.ra_col].values * u.rad
305 decs = fakeCat[self.config.dec_col].values * u.rad
306
307 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
308
309 # now use the exact pixel BBox to filter to only fakes that were inserted
310 xs = fakeCat["x"].values
311 ys = fakeCat["y"].values
312
313 bbox = Box2D(image.getBBox())
314 isContainedXy = xs >= bbox.minX
315 isContainedXy &= xs <= bbox.maxX
316 isContainedXy &= ys >= bbox.minY
317 isContainedXy &= ys <= bbox.maxY
318
319 return fakeCat[isContainedRaDec & isContainedXy]
320
321 def _getVectors(self, ras, decs):
322 """Convert ra dec to unit vectors on the sphere.
323
324 Parameters
325 ----------
326 ras : `numpy.ndarray`, (N,)
327 RA coordinates in radians.
328 decs : `numpy.ndarray`, (N,)
329 Dec coordinates in radians.
330
331 Returns
332 -------
333 vectors : `numpy.ndarray`, (N, 3)
334 Vectors on the unit sphere for the given RA/DEC values.
335 """
336 vectors = np.empty((len(ras), 3))
337
338 vectors[:, 2] = np.sin(decs)
339 vectors[:, 0] = np.cos(decs) * np.cos(ras)
340 vectors[:, 1] = np.cos(decs) * np.sin(ras)
341
342 return vectors
343
344
345class MatchVariableFakesConnections(MatchFakesConnections):
346 ccdVisitFakeMagnitudes = connTypes.Input(
347 doc="Catalog of fakes with magnitudes scattered for this ccdVisit.",
348 name="{fakesType}ccdVisitFakeMagnitudes",
349 storageClass="DataFrame",
350 dimensions=("instrument", "visit", "detector"),
351 )
352
353
354@deprecated(
355 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
356 version="v28.0",
357 category=FutureWarning,
358)
359class MatchVariableFakesConfig(MatchFakesConfig,
360 pipelineConnections=MatchVariableFakesConnections):
361 """Config for MatchFakesTask.
362 """
363 pass
364
365
366@deprecated(
367 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
368 version="v28.0",
369 category=FutureWarning,
370)
371class MatchVariableFakesTask(MatchFakesTask):
372 """Match injected fakes to their detected sources in the catalog and
373 compute their expected brightness in a difference image assuming perfect
374 subtraction.
375
376 This task is generally for injected sources that cannot be easily
377 identified by their footprints such as in the case of detector sources
378 post image differencing.
379 """
380 _DefaultName = "matchVariableFakes"
381 ConfigClass = MatchVariableFakesConfig
382
383 def runQuantum(self, butlerQC, inputRefs, outputRefs):
384 inputs = butlerQC.get(inputRefs)
385 inputs["band"] = butlerQC.quantum.dataId["band"]
386
387 outputs = self.run(**inputs)
388 butlerQC.put(outputs, outputRefs)
389
390 def run(self, fakeCats, ccdVisitFakeMagnitudes, skyMap, diffIm, associatedDiaSources, band):
391 """Match fakes to detected diaSources within a difference image bound.
392
393 Parameters
394 ----------
395 fakeCat : `pandas.DataFrame`
396 Catalog of fakes to match to detected diaSources.
397 diffIm : `lsst.afw.image.Exposure`
398 Difference image where ``associatedDiaSources`` were detected in.
399 associatedDiaSources : `pandas.DataFrame`
400 Catalog of difference image sources detected in ``diffIm``.
401
402 Returns
403 -------
404 result : `lsst.pipe.base.Struct`
405 Results struct with components.
406
407 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
408 length of ``fakeCat``. (`pandas.DataFrame`)
409 """
410 fakeCat = self.composeFakeCat(fakeCats, skyMap)
411 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band)
412 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
413
414 def computeExpectedDiffMag(self, fakeCat, ccdVisitFakeMagnitudes, band):
415 """Compute the magnitude expected in the difference image for this
416 detector/visit. Modify fakeCat in place.
417
418 Negative magnitudes indicate that the source should be detected as
419 a negative source.
420
421 Parameters
422 ----------
423 fakeCat : `pandas.DataFrame`
424 Catalog of fake sources.
425 ccdVisitFakeMagnitudes : `pandas.DataFrame`
426 Magnitudes for variable sources in this specific ccdVisit.
427 band : `str`
428 Band that this ccdVisit was observed in.
429 """
430 magName = self.config.mag_col % band
431 magnitudes = fakeCat[magName].to_numpy()
432 visitMags = ccdVisitFakeMagnitudes["variableMag"].to_numpy()
433 diffFlux = (visitMags * u.ABmag).to_value(u.nJy) - (magnitudes * u.ABmag).to_value(u.nJy)
434 diffMag = np.where(diffFlux > 0,
435 (diffFlux * u.nJy).to_value(u.ABmag),
436 -(-diffFlux * u.nJy).to_value(u.ABmag))
437
438 noVisit = ~fakeCat["isVisitSource"]
439 noTemplate = ~fakeCat["isTemplateSource"]
440 both = np.logical_and(fakeCat["isVisitSource"],
441 fakeCat["isTemplateSource"])
442
443 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit]
444 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate]
445 fakeCat.loc[both, magName] = diffMag[both]
A floating-point coordinate rectangle geometry.
Definition Box.h:413