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
40from deprecated.sphinx
import deprecated
44 defaultTemplates={
"coaddName":
"deep",
45 "fakesType":
"fakes_"},
46 dimensions=(
"instrument",
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",
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"),
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"),
70 associatedDiaSources = connTypes.Input(
71 doc=
"A DiaSource catalog to match against fakeCat. Assumed "
73 name=
"{fakesType}{coaddName}Diff_assocDiaSrc",
74 storageClass=
"DataFrame",
75 dimensions=(
"instrument",
"visit",
"detector"),
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"),
88 reason=
"This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
90 category=FutureWarning,
92class MatchFakesConfig(
94 pipelineConnections=MatchFakesConnections):
95 """Config for MatchFakesTask.
97 matchDistanceArcseconds = pexConfig.RangeField(
98 doc=
"Distance in arcseconds to match within.",
105 doMatchVisit = pexConfig.Field(
108 doc=
"Match visit to trim the fakeCat"
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.",
120 reason=
"This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
122 category=FutureWarning,
124class MatchFakesTask(PipelineTask):
125 """Match a pre-existing catalog of fakes to a catalog of detections on
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.
133 _DefaultName =
"matchFakes"
134 ConfigClass = MatchFakesConfig
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.
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``.
153 result : `lsst.pipe.base.Struct`
154 Results struct with components.
156 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
157 length of ``fakeCat``. (`pandas.DataFrame`)
159 fakeCat = self.composeFakeCat(fakeCats, skyMap)
161 if self.config.doMatchVisit:
162 fakeCat = self.getVisitMatchedFakeCat(fakeCat, diffIm)
164 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
166 def _processFakes(self, fakeCat, diffIm, associatedDiaSources):
167 """Match fakes to detected diaSources within a difference image bound.
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``.
180 result : `lsst.pipe.base.Struct`
181 Results struct with components.
183 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
184 length of ``fakeCat``. (`pandas.DataFrame`)
186 trimmedFakes = self._trimFakeCat(fakeCat, diffIm)
187 nPossibleFakes = len(trimmedFakes)
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"]))
195 diaSrcTree = cKDTree(diaSrcVects)
196 dist, idxs = diaSrcTree.query(
198 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
199 nFakesFound = np.isfinite(dist).sum()
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))
206 matchedDiaSources=matchedFakes.merge(
207 associatedDiaSources.reset_index(drop=
True), on=
"diaSourceId", how=
"left")
210 def composeFakeCat(self, fakeCats, skyMap):
211 """Concatenate the fakeCats from tracts that may cover the exposure.
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.
222 combinedFakeCat : `pandas.DataFrame`
223 All fakes that cover the inner polygon of the tracts in this
226 if len(fakeCats) == 1:
227 return fakeCats[0].get()
229 for fakeCatRef
in fakeCats:
230 cat = fakeCatRef.get()
231 tractId = fakeCatRef.dataId[
"tract"]
233 outputCat.append(cat[
234 skyMap.findTractIdArray(cat[self.config.ra_col],
235 cat[self.config.dec_col],
239 return pd.concat(outputCat)
241 def getVisitMatchedFakeCat(self, fakeCat, exposure):
242 """Trim the fakeCat to select particular visit
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
253 movingFakeCat : `pandas.DataFrame`
254 All fakes that belong to the visit
256 selected = exposure.getInfo().getVisitInfo().getId() == fakeCat[
"visit"]
258 return fakeCat[selected]
260 def _addPixCoords(self, fakeCat, image):
262 """Add pixel coordinates to the catalog of fakes.
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
272 fakeCat : `pandas.core.frame.DataFrame`
275 ras = fakeCat[self.config.ra_col].values
276 decs = fakeCat[self.config.dec_col].values
277 xs, ys = wcs.skyToPixelArray(ras, decs)
283 def _trimFakeCat(self, fakeCat, image):
284 """Trim the fake cat to the exact size of the input image.
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
294 fakeCat : `pandas.core.frame.DataFrame`
295 The original fakeCat trimmed to the area of the image
299 if (
'x' not in fakeCat.columns)
or (
'y' not in fakeCat.columns):
300 fakeCat = self._addPixCoords(fakeCat, image)
304 ras = fakeCat[self.config.ra_col].values * u.rad
305 decs = fakeCat[self.config.dec_col].values * u.rad
307 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
310 xs = fakeCat[
"x"].values
311 ys = fakeCat[
"y"].values
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
319 return fakeCat[isContainedRaDec & isContainedXy]
321 def _getVectors(self, ras, decs):
322 """Convert ra dec to unit vectors on the sphere.
326 ras : `numpy.ndarray`, (N,)
327 RA coordinates in radians.
328 decs : `numpy.ndarray`, (N,)
329 Dec coordinates in radians.
333 vectors : `numpy.ndarray`, (N, 3)
334 Vectors on the unit sphere for the given RA/DEC values.
336 vectors = np.empty((len(ras), 3))
338 vectors[:, 2] = np.sin(decs)
339 vectors[:, 0] = np.cos(decs) * np.cos(ras)
340 vectors[:, 1] = np.cos(decs) * np.sin(ras)
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"),
355 reason=
"This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
357 category=FutureWarning,
359class MatchVariableFakesConfig(MatchFakesConfig,
360 pipelineConnections=MatchVariableFakesConnections):
361 """Config for MatchFakesTask.
367 reason=
"This task will be removed in v28.0 as it is replaced by `source_injection` tasks.",
369 category=FutureWarning,
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
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.
380 _DefaultName =
"matchVariableFakes"
381 ConfigClass = MatchVariableFakesConfig
383 def runQuantum(self, butlerQC, inputRefs, outputRefs):
384 inputs = butlerQC.get(inputRefs)
385 inputs[
"band"] = butlerQC.quantum.dataId[
"band"]
387 outputs = self.run(**inputs)
388 butlerQC.put(outputs, outputRefs)
390 def run(self, fakeCats, ccdVisitFakeMagnitudes, skyMap, diffIm, associatedDiaSources, band):
391 """Match fakes to detected diaSources within a difference image bound.
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``.
404 result : `lsst.pipe.base.Struct`
405 Results struct with components.
407 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
408 length of ``fakeCat``. (`pandas.DataFrame`)
410 fakeCat = self.composeFakeCat(fakeCats, skyMap)
411 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band)
412 return self._processFakes(fakeCat, diffIm, associatedDiaSources)
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.
418 Negative magnitudes indicate that the source should be detected as
423 fakeCat : `pandas.DataFrame`
424 Catalog of fake sources.
425 ccdVisitFakeMagnitudes : `pandas.DataFrame`
426 Magnitudes for variable sources in this specific ccdVisit.
428 Band that this ccdVisit was observed in.
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))
438 noVisit = ~fakeCat[
"isVisitSource"]
439 noTemplate = ~fakeCat[
"isTemplateSource"]
440 both = np.logical_and(fakeCat[
"isVisitSource"],
441 fakeCat[
"isTemplateSource"])
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.