22__all__ = [
"MatchFakesTask",
24 "MatchVariableFakesConfig",
25 "MatchVariableFakesTask"]
27import astropy.units
as u
30from scipy.spatial
import cKDTree
34from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct
35import lsst.pipe.base.connectionTypes
as connTypes
42 defaultTemplates={
"coaddName":
"deep",
43 "fakesType":
"fakes_"},
44 dimensions=(
"instrument",
47 skyMap = connTypes.Input(
48 doc=
"Input definition of geometry/bbox and projection/wcs for "
49 "template exposures. Needed to test which tract to generate ",
50 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
51 dimensions=(
"skymap",),
52 storageClass=
"SkyMap",
54 fakeCats = connTypes.Input(
55 doc=
"Catalog of fake sources inserted into an image.",
56 name=
"{fakesType}fakeSourceCat",
57 storageClass=
"DataFrame",
58 dimensions=(
"tract",
"skymap"),
62 diffIm = connTypes.Input(
63 doc=
"Difference image on which the DiaSources were detected.",
64 name=
"{fakesType}{coaddName}Diff_differenceExp",
65 storageClass=
"ExposureF",
66 dimensions=(
"instrument",
"visit",
"detector"),
68 associatedDiaSources = connTypes.Input(
69 doc=
"A DiaSource catalog to match against fakeCat. Assumed "
71 name=
"{fakesType}{coaddName}Diff_assocDiaSrc",
72 storageClass=
"DataFrame",
73 dimensions=(
"instrument",
"visit",
"detector"),
75 matchedDiaSources = connTypes.Output(
76 doc=
"A catalog of those fakeCat sources that have a match in "
77 "associatedDiaSources. The schema is the union of the schemas for "
78 "``fakeCat`` and ``associatedDiaSources``.",
79 name=
"{fakesType}{coaddName}Diff_matchDiaSrc",
80 storageClass=
"DataFrame",
81 dimensions=(
"instrument",
"visit",
"detector"),
85class MatchFakesConfig(
87 pipelineConnections=MatchFakesConnections):
88 """Config for MatchFakesTask.
90 matchDistanceArcseconds = pexConfig.RangeField(
91 doc=
"Distance in arcseconds to match within.",
98 doMatchVisit = pexConfig.Field(
101 doc=
"Match visit to trim the fakeCat"
104 trimBuffer = pexConfig.Field(
105 doc=
"Size of the pixel buffer surrounding the image. Only those fake sources with a centroid"
106 "falling within the image+buffer region will be considered matches.",
112class MatchFakesTask(PipelineTask):
113 """Match a pre-existing catalog of fakes to a catalog of detections on
116 This task is generally for injected sources that cannot be easily
117 identified by their footprints such as in the case of detector sources
118 post image differencing.
121 _DefaultName =
"matchFakes"
122 ConfigClass = MatchFakesConfig
124 def run(self, fakeCats, skyMap, diffIm, associatedDiaSources):
125 """Compose fakes into a single catalog and match fakes to detected
126 diaSources within a difference image bound.
130 fakeCats : `pandas.DataFrame`
131 List of catalog of fakes to match to detected diaSources.
132 skyMap : `lsst.skymap.SkyMap`
133 SkyMap defining the tracts and patches the fakes are stored over.
134 diffIm : `lsst.afw.image.Exposure`
135 Difference image where ``associatedDiaSources`` were detected.
136 associatedDiaSources : `pandas.DataFrame`
137 Catalog of difference image sources detected in ``diffIm``.
141 result : `lsst.pipe.base.Struct`
142 Results struct with components.
144 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
145 length of ``fakeCat``. (`pandas.DataFrame`)
147 fakeCat = self.composeFakeCat(fakeCats, skyMap)
149 if self.config.doMatchVisit:
150 fakeCat = self.getVisitMatchedFakeCat(fakeCat, diffIm)
152 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
154 def _processFakes(self, fakeCat, diffIm, associatedDiaSources):
155 """Match fakes to detected diaSources within a difference image bound.
159 fakeCat : `pandas.DataFrame`
160 Catalog of fakes to match to detected diaSources.
161 diffIm : `lsst.afw.image.Exposure`
162 Difference image where ``associatedDiaSources`` were detected.
163 associatedDiaSources : `pandas.DataFrame`
164 Catalog of difference image sources detected in ``diffIm``.
168 result : `lsst.pipe.base.Struct`
169 Results struct with components.
171 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
172 length of ``fakeCat``. (`pandas.DataFrame`)
174 trimmedFakes = self._trimFakeCat(fakeCat, diffIm)
175 nPossibleFakes = len(trimmedFakes)
177 fakeVects = self._getVectors(trimmedFakes[self.config.ra_col],
178 trimmedFakes[self.config.dec_col])
179 diaSrcVects = self._getVectors(
180 np.radians(associatedDiaSources.loc[:,
"ra"]),
181 np.radians(associatedDiaSources.loc[:,
"dec"]))
183 diaSrcTree = cKDTree(diaSrcVects)
184 dist, idxs = diaSrcTree.query(
186 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
187 nFakesFound = np.isfinite(dist).sum()
189 self.log.info(
"Found %d out of %d possible.", nFakesFound, nPossibleFakes)
190 diaSrcIds = associatedDiaSources.iloc[np.where(np.isfinite(dist), idxs, 0)][
"diaSourceId"].to_numpy()
191 matchedFakes = trimmedFakes.assign(diaSourceId=np.where(np.isfinite(dist), diaSrcIds, 0))
194 matchedDiaSources=matchedFakes.merge(
195 associatedDiaSources.reset_index(drop=
True), on=
"diaSourceId", how=
"left")
198 def composeFakeCat(self, fakeCats, skyMap):
199 """Concatenate the fakeCats from tracts that may cover the exposure.
203 fakeCats : `list` of `lsst.daf.butler.DeferredDatasetHandle`
204 Set of fake cats to concatenate.
205 skyMap : `lsst.skymap.SkyMap`
206 SkyMap defining the geometry of the tracts and patches.
210 combinedFakeCat : `pandas.DataFrame`
211 All fakes that cover the inner polygon of the tracts in this
214 if len(fakeCats) == 1:
215 return fakeCats[0].get()
217 for fakeCatRef
in fakeCats:
218 cat = fakeCatRef.get()
219 tractId = fakeCatRef.dataId[
"tract"]
221 outputCat.append(cat[
222 skyMap.findTractIdArray(cat[self.config.ra_col],
223 cat[self.config.dec_col],
227 return pd.concat(outputCat)
229 def getVisitMatchedFakeCat(self, fakeCat, exposure):
230 """Trim the fakeCat to select particular visit
234 fakeCat : `pandas.core.frame.DataFrame`
235 The catalog of fake sources to add to the exposure
236 exposure : `lsst.afw.image.exposure.exposure.ExposureF`
237 The exposure to add the fake sources to
241 movingFakeCat : `pandas.DataFrame`
242 All fakes that belong to the visit
244 selected = exposure.getInfo().getVisitInfo().getId() == fakeCat[
"visit"]
246 return fakeCat[selected]
248 def _addPixCoords(self, fakeCat, image):
250 """Add pixel coordinates to the catalog of fakes.
254 fakeCat : `pandas.core.frame.DataFrame`
255 The catalog of fake sources to be input
256 image : `lsst.afw.image.exposure.exposure.ExposureF`
257 The image into which the fake sources should be added
260 fakeCat : `pandas.core.frame.DataFrame`
263 ras = fakeCat[self.config.ra_col].values
264 decs = fakeCat[self.config.dec_col].values
265 xs, ys = wcs.skyToPixelArray(ras, decs)
271 def _trimFakeCat(self, fakeCat, image):
272 """Trim the fake cat to the exact size of the input image.
276 fakeCat : `pandas.core.frame.DataFrame`
277 The catalog of fake sources that was input
278 image : `lsst.afw.image.exposure.exposure.ExposureF`
279 The image into which the fake sources were added
282 fakeCat : `pandas.core.frame.DataFrame`
283 The original fakeCat trimmed to the area of the image
287 if (
'x' not in fakeCat.columns)
or (
'y' not in fakeCat.columns):
288 fakeCat = self._addPixCoords(fakeCat, image)
292 ras = fakeCat[self.config.ra_col].values * u.rad
293 decs = fakeCat[self.config.dec_col].values * u.rad
295 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
298 xs = fakeCat[
"x"].values
299 ys = fakeCat[
"y"].values
301 bbox =
Box2D(image.getBBox())
302 isContainedXy = xs >= bbox.minX
303 isContainedXy &= xs <= bbox.maxX
304 isContainedXy &= ys >= bbox.minY
305 isContainedXy &= ys <= bbox.maxY
307 return fakeCat[isContainedRaDec & isContainedXy]
309 def _getVectors(self, ras, decs):
310 """Convert ra dec to unit vectors on the sphere.
314 ras : `numpy.ndarray`, (N,)
315 RA coordinates in radians.
316 decs : `numpy.ndarray`, (N,)
317 Dec coordinates in radians.
321 vectors : `numpy.ndarray`, (N, 3)
322 Vectors on the unit sphere for the given RA/DEC values.
324 vectors = np.empty((len(ras), 3))
326 vectors[:, 2] = np.sin(decs)
327 vectors[:, 0] = np.cos(decs) * np.cos(ras)
328 vectors[:, 1] = np.cos(decs) * np.sin(ras)
334 ccdVisitFakeMagnitudes = connTypes.Input(
335 doc=
"Catalog of fakes with magnitudes scattered for this ccdVisit.",
336 name=
"{fakesType}ccdVisitFakeMagnitudes",
337 storageClass=
"DataFrame",
338 dimensions=(
"instrument",
"visit",
"detector"),
342class MatchVariableFakesConfig(MatchFakesConfig,
343 pipelineConnections=MatchVariableFakesConnections):
344 """Config for MatchFakesTask.
349class MatchVariableFakesTask(MatchFakesTask):
350 """Match injected fakes to their detected sources in the catalog and
351 compute their expected brightness in a difference image assuming perfect
354 This task is generally for injected sources that cannot be easily
355 identified by their footprints such as in the case of detector sources
356 post image differencing.
358 _DefaultName =
"matchVariableFakes"
359 ConfigClass = MatchVariableFakesConfig
361 def runQuantum(self, butlerQC, inputRefs, outputRefs):
362 inputs = butlerQC.get(inputRefs)
363 inputs[
"band"] = butlerQC.quantum.dataId[
"band"]
365 outputs = self.run(**inputs)
366 butlerQC.put(outputs, outputRefs)
368 def run(self, fakeCats, ccdVisitFakeMagnitudes, skyMap, diffIm, associatedDiaSources, band):
369 """Match fakes to detected diaSources within a difference image bound.
373 fakeCat : `pandas.DataFrame`
374 Catalog of fakes to match to detected diaSources.
375 diffIm : `lsst.afw.image.Exposure`
376 Difference image where ``associatedDiaSources`` were detected in.
377 associatedDiaSources : `pandas.DataFrame`
378 Catalog of difference image sources detected in ``diffIm``.
382 result : `lsst.pipe.base.Struct`
383 Results struct with components.
385 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
386 length of ``fakeCat``. (`pandas.DataFrame`)
388 fakeCat = self.composeFakeCat(fakeCats, skyMap)
389 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band)
390 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
392 def computeExpectedDiffMag(self, fakeCat, ccdVisitFakeMagnitudes, band):
393 """Compute the magnitude expected in the difference image for this
394 detector/visit. Modify fakeCat in place.
396 Negative magnitudes indicate that the source should be detected as
401 fakeCat : `pandas.DataFrame`
402 Catalog of fake sources.
403 ccdVisitFakeMagnitudes : `pandas.DataFrame`
404 Magnitudes for variable sources in this specific ccdVisit.
406 Band that this ccdVisit was observed in.
408 magName = self.config.mag_col % band
409 magnitudes = fakeCat[magName].to_numpy()
410 visitMags = ccdVisitFakeMagnitudes[
"variableMag"].to_numpy()
411 diffFlux = (visitMags * u.ABmag).to_value(u.nJy) - (magnitudes * u.ABmag).to_value(u.nJy)
412 diffMag = np.where(diffFlux > 0,
413 (diffFlux * u.nJy).to_value(u.ABmag),
414 -(-diffFlux * u.nJy).to_value(u.ABmag))
416 noVisit = ~fakeCat[
"isVisitSource"]
417 noTemplate = ~fakeCat[
"isTemplateSource"]
418 both = np.logical_and(fakeCat[
"isVisitSource"],
419 fakeCat[
"isTemplateSource"])
421 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit]
422 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate]
423 fakeCat.loc[both, magName] = diffMag[both]
A floating-point coordinate rectangle geometry.