22import astropy.units
as u
25from scipy.spatial
import cKDTree
29from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct
30import lsst.pipe.base.connectionTypes
as connTypes
35__all__ = [
"MatchFakesTask",
37 "MatchVariableFakesConfig",
38 "MatchVariableFakesTask"]
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.
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.
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[:,
"decl"]))
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 `lst.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(
216 datasetType=self.config.connections.fakeCats)
218 for fakeCatRef
in fakeCats:
219 cat = fakeCatRef.get(
220 datasetType=self.config.connections.fakeCats)
221 tractId = fakeCatRef.dataId[
"tract"]
223 outputCat.append(cat[
224 skyMap.findTractIdArray(cat[self.config.ra_col],
225 cat[self.config.dec_col],
229 return pd.concat(outputCat)
231 def getVisitMatchedFakeCat(self, fakeCat, exposure):
232 """Trim the fakeCat to select particular visit
236 fakeCat : `pandas.core.frame.DataFrame`
237 The catalog of fake sources to add to the exposure
238 exposure : `lsst.afw.image.exposure.exposure.ExposureF`
239 The exposure to add the fake sources to
243 movingFakeCat : `pandas.DataFrame`
244 All fakes that belong to the visit
246 selected = exposure.getInfo().getVisitInfo().getId() == fakeCat["visit"]
248 return fakeCat[selected]
250 def _addPixCoords(self, fakeCat, image):
252 """Add pixel coordinates to the catalog of fakes.
256 fakeCat : `pandas.core.frame.DataFrame`
257 The catalog of fake sources to be input
258 image : `lsst.afw.image.exposure.exposure.ExposureF`
259 The image into which the fake sources should be added
262 fakeCat : `pandas.core.frame.DataFrame`
265 ras = fakeCat[self.config.ra_col].values
266 decs = fakeCat[self.config.dec_col].values
267 xs, ys = wcs.skyToPixelArray(ras, decs)
273 def _trimFakeCat(self, fakeCat, image):
274 """Trim the fake cat to the exact size of the input image.
278 fakeCat : `pandas.core.frame.DataFrame`
279 The catalog of fake sources that was input
280 image : `lsst.afw.image.exposure.exposure.ExposureF`
281 The image into which the fake sources were added
284 fakeCat : `pandas.core.frame.DataFrame`
285 The original fakeCat trimmed to the area of the image
289 if (
'x' not in fakeCat.columns)
or (
'y' not in fakeCat.columns):
290 fakeCat = self._addPixCoords(fakeCat, image)
294 ras = fakeCat[self.config.ra_col].values * u.rad
295 decs = fakeCat[self.config.dec_col].values * u.rad
297 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
300 xs = fakeCat[
"x"].values
301 ys = fakeCat[
"y"].values
303 bbox =
Box2D(image.getBBox())
304 isContainedXy = xs >= bbox.minX
305 isContainedXy &= xs <= bbox.maxX
306 isContainedXy &= ys >= bbox.minY
307 isContainedXy &= ys <= bbox.maxY
309 return fakeCat[isContainedRaDec & isContainedXy]
311 def _getVectors(self, ras, decs):
312 """Convert ra dec to unit vectors on the sphere.
316 ras : `numpy.ndarray`, (N,)
317 RA coordinates in radians.
318 decs : `numpy.ndarray`, (N,)
319 Dec coordinates
in radians.
323 vectors : `numpy.ndarray`, (N, 3)
324 Vectors on the unit sphere
for the given RA/DEC values.
326 vectors = np.empty((len(ras), 3))
328 vectors[:, 2] = np.sin(decs)
329 vectors[:, 0] = np.cos(decs) * np.cos(ras)
330 vectors[:, 1] = np.cos(decs) * np.sin(ras)
336 ccdVisitFakeMagnitudes = connTypes.Input(
337 doc=
"Catalog of fakes with magnitudes scattered for this ccdVisit.",
338 name=
"{fakesType}ccdVisitFakeMagnitudes",
339 storageClass=
"DataFrame",
340 dimensions=(
"instrument",
"visit",
"detector"),
344class MatchVariableFakesConfig(MatchFakesConfig,
345 pipelineConnections=MatchVariableFakesConnections):
346 """Config for MatchFakesTask.
351class MatchVariableFakesTask(MatchFakesTask):
352 """Match injected fakes to their detected sources in the catalog and
353 compute their expected brightness in a difference image assuming perfect
356 This task
is generally
for injected sources that cannot be easily
357 identified by their footprints such
as in the case of detector sources
358 post image differencing.
360 _DefaultName = "matchVariableFakes"
361 ConfigClass = MatchVariableFakesConfig
363 def runQuantum(self, butlerQC, inputRefs, outputRefs):
364 inputs = butlerQC.get(inputRefs)
365 inputs[
"band"] = butlerQC.quantum.dataId[
"band"]
367 outputs = self.run(**inputs)
368 butlerQC.put(outputs, outputRefs)
370 def run(self, fakeCats, ccdVisitFakeMagnitudes, skyMap, diffIm, associatedDiaSources, band):
371 """Match fakes to detected diaSources within a difference image bound.
375 fakeCat : `pandas.DataFrame`
376 Catalog of fakes to match to detected diaSources.
378 Difference image where ``associatedDiaSources`` were detected in.
379 associatedDiaSources : `pandas.DataFrame`
380 Catalog of difference image sources detected
in ``diffIm``.
384 result : `lsst.pipe.base.Struct`
385 Results struct
with components.
387 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
388 length of ``fakeCat``. (`pandas.DataFrame`)
390 fakeCat = self.composeFakeCat(fakeCats, skyMap)
391 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band)
392 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
394 def computeExpectedDiffMag(self, fakeCat, ccdVisitFakeMagnitudes, band):
395 """Compute the magnitude expected in the difference image for this
396 detector/visit. Modify fakeCat in place.
398 Negative magnitudes indicate that the source should be detected
as
403 fakeCat : `pandas.DataFrame`
404 Catalog of fake sources.
405 ccdVisitFakeMagnitudes : `pandas.DataFrame`
406 Magnitudes
for variable sources
in this specific ccdVisit.
408 Band that this ccdVisit was observed
in.
410 magName = self.config.mag_col % band
411 magnitudes = fakeCat[magName].to_numpy()
412 visitMags = ccdVisitFakeMagnitudes["variableMag"].to_numpy()
413 diffFlux = (visitMags * u.ABmag).to_value(u.nJy) - (magnitudes * u.ABmag).to_value(u.nJy)
414 diffMag = np.where(diffFlux > 0,
415 (diffFlux * u.nJy).to_value(u.ABmag),
416 -(-diffFlux * u.nJy).to_value(u.ABmag))
418 noVisit = ~fakeCat[
"isVisitSource"]
419 noTemplate = ~fakeCat[
"isTemplateSource"]
420 both = np.logical_and(fakeCat[
"isVisitSource"],
421 fakeCat[
"isTemplateSource"])
423 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit]
424 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate]
425 fakeCat.loc[both, magName] = diffMag[both]
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A floating-point coordinate rectangle geometry.
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)