22 import astropy.units
as u
24 from scipy.spatial
import cKDTree
26 from lsst.geom import Box2D, radians, SpherePoint
28 from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct
29 import lsst.pipe.base.connectionTypes
as connTypes
32 __all__ = [
"MatchFakesTask",
34 "MatchVariableFakesConfig",
35 "MatchVariableFakesTask"]
39 defaultTemplates={
"coaddName":
"deep",
40 "fakesType":
"fakes_"},
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")
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"),
58 associatedDiaSources = connTypes.Input(
59 doc=
"A DiaSource catalog to match against fakeCat. Assumed "
61 name=
"{fakesType}{coaddName}Diff_assocDiaSrc",
62 storageClass=
"DataFrame",
63 dimensions=(
"instrument",
"visit",
"detector"),
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"),
75 class MatchFakesConfig(
77 pipelineConnections=MatchFakesConnections):
78 """Config for MatchFakesTask.
80 matchDistanceArcseconds = pexConfig.RangeField(
81 doc=
"Distance in arcseconds to match within.",
89 class MatchFakesTask(PipelineTask):
90 """Match a pre-existing catalog of fakes to a catalog of detections on
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.
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.
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``.
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`)
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(
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))
147 matchedDiaSources=matchedFakes.merge(
148 associatedDiaSources.reset_index(drop=
True), on=
"diaSourceId", how=
"left")
151 def _trimFakeCat(self, fakeCat, image):
152 """Trim the fake cat to about the size of the input image.
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
163 fakeCat : `pandas.core.frame.DataFrame`
164 The original fakeCat trimmed to the area of the image
168 bbox =
Box2D(image.getBBox())
172 row[self.config.decColName],
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.
184 ras : `numpy.ndarray`, (N,)
185 RA coordinates in radians.
186 decs : `numpy.ndarray`, (N,)
187 Dec coordinates in radians.
191 vectors : `numpy.ndarray`, (N, 3)
192 Vectors on the unit sphere for the given RA/DEC values.
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)
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"),
212 class MatchVariableFakesConfig(MatchFakesConfig,
213 pipelineConnections=MatchVariableFakesConnections):
214 """Config for MatchFakesTask.
219 class 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
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.
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.
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``.
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`)
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
270 fakeCat : `pandas.DataFrame`
271 Catalog of fake sources.
272 ccdVisitFakeMagnitudes : `pandas.DataFrame`
273 Magnitudes for variable sources in this specific ccdVisit.
275 Band that this ccdVisit was observed in.
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]
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
def run(self, coaddExposures, bbox, wcs)