22__all__ = [
"MatchInjectedToDiaSourceTask",
23 "MatchInjectedToDiaSourceConfig",
24 "MatchInjectedToAssocDiaSourceTask",
25 "MatchInjectedToAssocDiaSourceConfig"]
27import astropy.units
as u
29from scipy.spatial
import cKDTree
31from lsst.afw import table
as afwTable
32from lsst
import geom
as lsstGeom
34from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
35import lsst.pipe.base.connectionTypes
as connTypes
36from lsst.meas.base import ForcedMeasurementTask, ForcedMeasurementConfig
40 PipelineTaskConnections,
41 defaultTemplates={
"coaddName":
"deep",
42 "fakesType":
"fakes_"},
43 dimensions=(
"instrument",
46 injectedCat = connTypes.Input(
47 doc=
"Catalog of sources injected in the images.",
48 name=
"{fakesType}_pvi_catalog",
49 storageClass=
"ArrowAstropy",
50 dimensions=(
"instrument",
"visit",
"detector"),
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 diaSources = connTypes.Input(
59 doc=
"A DiaSource catalog to match against fakeCat.",
60 name=
"{fakesType}{coaddName}Diff_diaSrc",
61 storageClass=
"SourceCatalog",
62 dimensions=(
"instrument",
"visit",
"detector"),
64 matchDiaSources = connTypes.Output(
65 doc=
"A catalog of those fakeCat sources that have a match in "
66 "diaSrc. The schema is the union of the schemas for "
67 "``fakeCat`` and ``diaSrc``.",
68 name=
"{fakesType}{coaddName}Diff_matchDiaSrc",
69 storageClass=
"DataFrame",
70 dimensions=(
"instrument",
"visit",
"detector"),
74class MatchInjectedToDiaSourceConfig(
76 pipelineConnections=MatchInjectedToDiaSourceConnections):
77 """Config for MatchFakesTask.
79 matchDistanceArcseconds = pexConfig.RangeField(
80 doc=
"Distance in arcseconds to match within.",
86 doMatchVisit = pexConfig.Field(
89 doc=
"Match visit to trim the fakeCat"
91 trimBuffer = pexConfig.Field(
92 doc=
"Size of the pixel buffer surrounding the image."
93 "Only those fake sources with a centroid"
94 "falling within the image+buffer region will be considered matches.",
98 doForcedMeasurement = pexConfig.Field(
101 doc=
"Force measurement of the fakes at the injection locations."
103 forcedMeasurement = pexConfig.ConfigurableField(
104 target=ForcedMeasurementTask,
105 doc=
"Task to force photometer difference image at injection locations.",
109class MatchInjectedToDiaSourceTask(PipelineTask):
111 _DefaultName =
"matchInjectedToDiaSource"
112 ConfigClass = MatchInjectedToDiaSourceConfig
114 def run(self, injectedCat, diffIm, diaSources):
115 """Match injected sources to detected diaSources within a difference image bound.
119 injectedCat : `astropy.table.table.Table`
120 Table of catalog of synthetic sources to match to detected diaSources.
121 diffIm : `lsst.afw.image.Exposure`
122 Difference image where ``diaSources`` were detected.
123 diaSources : `afw.table.SourceCatalog`
124 Catalog of difference image sources detected in ``diffIm``.
127 result : `lsst.pipe.base.Struct`
128 Results struct with components.
130 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
131 length of ``injectedCalexpCat``. (`pandas.DataFrame`)
134 if self.config.doMatchVisit:
135 fakeCat = self._trimFakeCat(injectedCat, diffIm)
137 fakeCat = injectedCat
138 if self.config.doForcedMeasurement:
139 self._estimateFakesSNR(fakeCat, diffIm)
141 return self._processFakes(fakeCat, diaSources)
143 def _estimateFakesSNR(self, injectedCat, diffIm):
144 """Estimate the signal-to-noise ratio of the fakes in the given catalog.
148 injectedCat : `astropy.table.Table`
149 Catalog of synthetic sources to estimate the S/N of. **This table
150 will be modified in place**.
151 diffIm : `lsst.afw.image.Exposure`
152 Difference image where the sources were detected.
155 schema = afwTable.SourceTable.makeMinimalSchema()
156 schema.addField(
"x",
"D",
"x position in image.", units=
"pixel")
157 schema.addField(
"y",
"D",
"y position in image.", units=
"pixel")
158 schema.addField(
"deblend_nChild",
"I",
"Need for minimal forced phot schema")
163 "base_CircularApertureFlux",
165 "base_LocalBackground"
168 forcedMeasConfig.slots.centroid =
'base_SdssCentroid'
169 forcedMeasConfig.slots.shape =
None
175 forcedMeas.copyColumns = {
"coord_ra":
"ra",
"coord_dec":
"dec"}
179 outputCatalog.reserve(len(injectedCat))
180 for row
in injectedCat:
181 outputRecord = outputCatalog.addNew()
182 outputRecord.setId(row[
'injection_id'])
183 outputRecord.setCoord(lsstGeom.SpherePoint(row[
"ra"], row[
"dec"], lsstGeom.degrees))
184 outputRecord.set(
"x", row[
"x"])
185 outputRecord.set(
"y", row[
"y"])
188 forcedSources = forcedMeas.generateMeasCat(diffIm, outputCatalog, diffIm.getWcs())
190 forcedMeas.attachPsfShapeFootprints(forcedSources, diffIm)
194 for src, tgt
in zip(forcedSources, outputCatalog):
195 src.set(
'base_SdssCentroid_x', tgt[
'x'])
196 src.set(
'base_SdssCentroid_y', tgt[
'y'])
199 forcedSources.defineCentroid(
'base_SdssCentroid')
201 forcedMeas.run(forcedSources, diffIm, outputCatalog, diffIm.getWcs())
203 forcedSources_table = forcedSources.asAstropy()
206 for column
in forcedSources_table.columns:
207 if "Flux" in column
or "flag" in column:
208 injectedCat[
"forced_"+column] = forcedSources_table[column]
211 for column
in injectedCat.colnames:
212 if column.endswith(
"instFlux"):
213 flux = injectedCat[column]
214 fluxErr = injectedCat[column+
"Err"].copy()
216 (fluxErr <= 0) | (np.isnan(fluxErr)), np.nanmax(fluxErr), fluxErr)
218 injectedCat[column+
"_SNR"] = flux / fluxErr
220 def _processFakes(self, injectedCat, diaSources):
221 """Match fakes to detected diaSources within a difference image bound.
225 injectedCat : `astropy.table.table.Table`
226 Catalog of injected sources to match to detected diaSources.
227 diaSources : `afw.table.SourceCatalog`
228 Catalog of difference image sources detected in ``diffIm``.
229 associatedDiaSources : `pandas.DataFrame`
230 Catalog of associated difference image sources detected in ``diffIm``.
234 result : `lsst.pipe.base.Struct`
235 Results struct with components.
237 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
238 length of ``fakeCat``. (`pandas.DataFrame`)
241 injectedCat = injectedCat.to_pandas()
242 nPossibleFakes = len(injectedCat)
244 fakeVects = self._getVectors(
245 np.radians(injectedCat.ra),
246 np.radians(injectedCat.dec))
247 diaSrcVects = self._getVectors(
248 diaSources[
'coord_ra'],
249 diaSources[
'coord_dec'])
251 diaSrcTree = cKDTree(diaSrcVects)
252 dist, idxs = diaSrcTree.query(
254 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
255 nFakesFound = np.isfinite(dist).sum()
257 self.log.info(
"Found %d out of %d possible in diaSources.", nFakesFound, nPossibleFakes)
260 diaSrcIds = diaSources[
'id'][np.where(np.isfinite(dist), idxs, 0)]
261 matchedFakes = injectedCat.assign(diaSourceId=np.where(np.isfinite(dist), diaSrcIds, 0))
262 matchedFakes[
'dist_diaSrc'] = np.where(np.isfinite(dist), 3600*np.rad2deg(dist), -1)
264 return Struct(matchDiaSources=matchedFakes)
266 def _getVectors(self, ras, decs):
267 """Convert ra dec to unit vectors on the sphere.
271 ras : `numpy.ndarray`, (N,)
272 RA coordinates in radians.
273 decs : `numpy.ndarray`, (N,)
274 Dec coordinates in radians.
278 vectors : `numpy.ndarray`, (N, 3)
279 Vectors on the unit sphere for the given RA/DEC values.
281 vectors = np.empty((len(ras), 3))
283 vectors[:, 2] = np.sin(decs)
284 vectors[:, 0] = np.cos(decs) * np.cos(ras)
285 vectors[:, 1] = np.cos(decs) * np.sin(ras)
289 def _addPixCoords(self, fakeCat, image):
290 """Add pixel coordinates to the catalog of fakes.
294 fakeCat : `astropy.table.table.Table`
295 The catalog of fake sources to be input
296 image : `lsst.afw.image.exposure.exposure.ExposureF`
297 The image into which the fake sources should be added
300 fakeCat : `astropy.table.table.Table`
306 xs, ys = wcs.skyToPixelArray(
316 def _trimFakeCat(self, fakeCat, image):
317 """Trim the fake cat to the exact size of the input image.
321 fakeCat : `astropy.table.table.Table`
322 The catalog of fake sources that was input
323 image : `lsst.afw.image.exposure.exposure.ExposureF`
324 The image into which the fake sources were added
327 fakeCat : `astropy.table.table.Table`
328 The original fakeCat trimmed to the area of the image
332 fakeCat = self._addPixCoords(fakeCat, image)
336 ras = fakeCat[
"ra"] * u.deg
337 decs = fakeCat[
"dec"] * u.deg
339 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
345 bbox = lsstGeom.Box2D(image.getBBox())
346 isContainedXy = xs - self.config.trimBuffer >= bbox.minX
347 isContainedXy &= xs + self.config.trimBuffer <= bbox.maxX
348 isContainedXy &= ys - self.config.trimBuffer >= bbox.minY
349 isContainedXy &= ys + self.config.trimBuffer <= bbox.maxY
351 return fakeCat[isContainedRaDec & isContainedXy]
354class MatchInjectedToAssocDiaSourceConnections(
355 PipelineTaskConnections,
356 defaultTemplates={
"coaddName":
"deep",
357 "fakesType":
"fakes_"},
358 dimensions=(
"instrument",
362 assocDiaSources = connTypes.Input(
363 doc=
"An assocDiaSource catalog to match against fakeCat from the"
364 "diaPipe run. Assumed to be SDMified.",
365 name=
"{fakesType}{coaddName}Diff_assocDiaSrc",
366 storageClass=
"DataFrame",
367 dimensions=(
"instrument",
"visit",
"detector"),
369 matchDiaSources = connTypes.Input(
370 doc=
"A catalog of those fakeCat sources that have a match in "
371 "diaSrc. The schema is the union of the schemas for "
372 "``fakeCat`` and ``diaSrc``.",
373 name=
"{fakesType}{coaddName}Diff_matchDiaSrc",
374 storageClass=
"DataFrame",
375 dimensions=(
"instrument",
"visit",
"detector"),
377 matchAssocDiaSources = connTypes.Output(
378 doc=
"A catalog of those fakeCat sources that have a match in "
379 "associatedDiaSources. The schema is the union of the schemas for "
380 "``fakeCat`` and ``associatedDiaSources``.",
381 name=
"{fakesType}{coaddName}Diff_matchAssocDiaSrc",
382 storageClass=
"DataFrame",
383 dimensions=(
"instrument",
"visit",
"detector"),
387class MatchInjectedToAssocDiaSourceConfig(
389 pipelineConnections=MatchInjectedToAssocDiaSourceConnections):
390 """Config for MatchFakesTask.
394class MatchInjectedToAssocDiaSourceTask(PipelineTask):
396 _DefaultName =
"matchInjectedToAssocDiaSource"
397 ConfigClass = MatchInjectedToAssocDiaSourceConfig
399 def run(self, assocDiaSources, matchDiaSources):
400 """Tag matched injected sources to associated diaSources.
404 matchDiaSources : `pandas.DataFrame`
405 Catalog of matched diaSrc to injected sources
406 assocDiaSources : `pandas.DataFrame`
407 Catalog of associated difference image sources detected in ``diffIm``.
410 result : `lsst.pipe.base.Struct`
411 Results struct with components.
413 - ``matchAssocDiaSources`` : Fakes matched to associated diaSources. Has
414 length of ``matchDiaSources``. (`pandas.DataFrame`)
418 nPossibleFakes = len(matchDiaSources)
419 matchDiaSources[
'isAssocDiaSource'] = matchDiaSources.diaSourceId.isin(assocDiaSources.diaSourceId)
420 assocNFakesFound = matchDiaSources.isAssocDiaSource.sum()
421 self.log.info(
"Found %d out of %d possible in assocDiaSources."%(assocNFakesFound, nPossibleFakes))
424 matchAssocDiaSources=matchDiaSources.merge(
425 assocDiaSources.reset_index(drop=
True),
428 suffixes=(
'_ssi',
'_diaSrc')