LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+b203dec576,g18429d2f64+358861cd2c,g199a45376c+0ba108daf9,g1fd858c14a+dd066899e3,g262e1987ae+ebfced1d55,g29ae962dfc+72fd90588e,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+b668f15bc5,g4595892280+3897dae354,g47891489e3+abcf9c3559,g4d44eb3520+fb4ddce128,g53246c7159+8c5ae1fdc5,g67b6fd64d1+abcf9c3559,g67fd3c3899+1f72b5a9f7,g74acd417e5+cb6b47f07b,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+abcf9c3559,g8d7436a09f+bcf525d20c,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+6054cc57f1,g97be763408+06f794da49,g9dd6db0277+1f72b5a9f7,ga681d05dcb+7e36ad54cd,gabf8522325+735880ea63,gac2eed3f23+abcf9c3559,gb89ab40317+abcf9c3559,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+1f72b5a9f7,gdab6d2f7ff+cb6b47f07b,gdc713202bf+1f72b5a9f7,gdfd2d52018+8225f2b331,ge365c994fd+375fc21c71,ge410e46f29+abcf9c3559,geaed405ab2+562b3308c0,gf9a733ac38+8c5ae1fdc5,w.2025.35
LSST Data Management Base Package
Loading...
Searching...
No Matches
matchDiffimSourceInjected.py
Go to the documentation of this file.
1# This file is part of ap_pipe.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ["MatchInjectedToDiaSourceTask",
23 "MatchInjectedToDiaSourceConfig",
24 "MatchInjectedToAssocDiaSourceTask",
25 "MatchInjectedToAssocDiaSourceConfig"]
26
27import astropy.units as u
28import numpy as np
29from scipy.spatial import cKDTree
30
31from lsst.afw import table as afwTable
32from lsst import geom as lsstGeom
33import lsst.pex.config as pexConfig
34from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
35import lsst.pipe.base.connectionTypes as connTypes
36from lsst.meas.base import ForcedMeasurementTask, ForcedMeasurementConfig
37
38
40 PipelineTaskConnections,
41 defaultTemplates={"coaddName": "deep",
42 "fakesType": "fakes_"},
43 dimensions=("instrument",
44 "visit",
45 "detector")):
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"),
51 )
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"),
57 )
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"),
63 )
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"),
71 )
72
73
74class MatchInjectedToDiaSourceConfig(
75 PipelineTaskConfig,
76 pipelineConnections=MatchInjectedToDiaSourceConnections):
77 """Config for MatchFakesTask.
78 """
79 matchDistanceArcseconds = pexConfig.RangeField(
80 doc="Distance in arcseconds to match within.",
81 dtype=float,
82 default=0.5,
83 min=0,
84 max=10,
85 )
86 doMatchVisit = pexConfig.Field(
87 dtype=bool,
88 default=True,
89 doc="Match visit to trim the fakeCat"
90 )
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.",
95 dtype=int,
96 default=50,
97 )
98 doForcedMeasurement = pexConfig.Field(
99 dtype=bool,
100 default=True,
101 doc="Force measurement of the fakes at the injection locations."
102 )
103 forcedMeasurement = pexConfig.ConfigurableField(
104 target=ForcedMeasurementTask,
105 doc="Task to force photometer difference image at injection locations.",
106 )
107
108
109class MatchInjectedToDiaSourceTask(PipelineTask):
110
111 _DefaultName = "matchInjectedToDiaSource"
112 ConfigClass = MatchInjectedToDiaSourceConfig
113
114 def run(self, injectedCat, diffIm, diaSources):
115 """Match injected sources to detected diaSources within a difference image bound.
116
117 Parameters
118 ----------
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``.
125 Returns
126 -------
127 result : `lsst.pipe.base.Struct`
128 Results struct with components.
129
130 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
131 length of ``injectedCalexpCat``. (`pandas.DataFrame`)
132 """
133
134 if self.config.doMatchVisit:
135 fakeCat = self._trimFakeCat(injectedCat, diffIm)
136 else:
137 fakeCat = injectedCat
138 if self.config.doForcedMeasurement:
139 self._estimateFakesSNR(fakeCat, diffIm)
140
141 return self._processFakes(fakeCat, diaSources)
142
143 def _estimateFakesSNR(self, injectedCat, diffIm):
144 """Estimate the signal-to-noise ratio of the fakes in the given catalog.
145
146 Parameters
147 ----------
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.
153 """
154 # Create a schema for the forced measurement task
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")
159
160 pluginList = [
161 "base_PixelFlags",
162 "base_SdssCentroid",
163 "base_CircularApertureFlux",
164 "base_PsfFlux",
165 "base_LocalBackground"
166 ]
167 forcedMeasConfig = ForcedMeasurementConfig(plugins=pluginList)
168 forcedMeasConfig.slots.centroid = 'base_SdssCentroid'
169 forcedMeasConfig.slots.shape = None
170
171 # Create the forced measurement task
172 forcedMeas = ForcedMeasurementTask(schema, config=forcedMeasConfig)
173
174 # Specify the columns to copy from the input catalog to the output catalog
175 forcedMeas.copyColumns = {"coord_ra": "ra", "coord_dec": "dec"}
176
177 # Create an afw table from the input catalog
178 outputCatalog = afwTable.SourceCatalog(schema)
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"])
186
187 # Generate the forced measurement catalog
188 forcedSources = forcedMeas.generateMeasCat(diffIm, outputCatalog, diffIm.getWcs())
189 # Attach the PSF shape footprints to the forced measurement catalog
190 forcedMeas.attachPsfShapeFootprints(forcedSources, diffIm)
191
192 # Copy the x and y positions from the forced measurement catalog back
193 # to the input catalog
194 for src, tgt in zip(forcedSources, outputCatalog):
195 src.set('base_SdssCentroid_x', tgt['x'])
196 src.set('base_SdssCentroid_y', tgt['y'])
197
198 # Define the centroid for the forced measurement catalog
199 forcedSources.defineCentroid('base_SdssCentroid')
200 # Run the forced measurement task
201 forcedMeas.run(forcedSources, diffIm, outputCatalog, diffIm.getWcs())
202 # Convert the forced measurement catalog to an astropy table
203 forcedSources_table = forcedSources.asAstropy()
204
205 # Add the forced measurement columns to the input catalog
206 for column in forcedSources_table.columns:
207 if "Flux" in column or "flag" in column:
208 injectedCat["forced_"+column] = forcedSources_table[column]
209
210 # Add the SNR columns to the input catalog
211 for column in injectedCat.colnames:
212 if column.endswith("instFlux"):
213 flux = injectedCat[column]
214 fluxErr = injectedCat[column+"Err"].copy()
215 fluxErr = np.where(
216 (fluxErr <= 0) | (np.isnan(fluxErr)), np.nanmax(fluxErr), fluxErr)
217
218 injectedCat[column+"_SNR"] = flux / fluxErr
219
220 def _processFakes(self, injectedCat, diaSources):
221 """Match fakes to detected diaSources within a difference image bound.
222
223 Parameters
224 ----------
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``.
231
232 Returns
233 -------
234 result : `lsst.pipe.base.Struct`
235 Results struct with components.
236
237 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has
238 length of ``fakeCat``. (`pandas.DataFrame`)
239 """
240 # First match the diaSrc to the injected fakes
241 injectedCat = injectedCat.to_pandas()
242 nPossibleFakes = len(injectedCat)
243
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'])
250
251 diaSrcTree = cKDTree(diaSrcVects)
252 dist, idxs = diaSrcTree.query(
253 fakeVects,
254 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600))
255 nFakesFound = np.isfinite(dist).sum()
256
257 self.log.info("Found %d out of %d possible in diaSources.", nFakesFound, nPossibleFakes)
258
259 # assign diaSourceId to the matched fakes
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)
263
264 return Struct(matchDiaSources=matchedFakes)
265
266 def _getVectors(self, ras, decs):
267 """Convert ra dec to unit vectors on the sphere.
268
269 Parameters
270 ----------
271 ras : `numpy.ndarray`, (N,)
272 RA coordinates in radians.
273 decs : `numpy.ndarray`, (N,)
274 Dec coordinates in radians.
275
276 Returns
277 -------
278 vectors : `numpy.ndarray`, (N, 3)
279 Vectors on the unit sphere for the given RA/DEC values.
280 """
281 vectors = np.empty((len(ras), 3))
282
283 vectors[:, 2] = np.sin(decs)
284 vectors[:, 0] = np.cos(decs) * np.cos(ras)
285 vectors[:, 1] = np.cos(decs) * np.sin(ras)
286
287 return vectors
288
289 def _addPixCoords(self, fakeCat, image):
290 """Add pixel coordinates to the catalog of fakes.
291
292 Parameters
293 ----------
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
298 Returns
299 -------
300 fakeCat : `astropy.table.table.Table`
301 """
302
303 wcs = image.getWcs()
304
305 # Get x/y pixel coordinates for injected sources.
306 xs, ys = wcs.skyToPixelArray(
307 fakeCat["ra"],
308 fakeCat["dec"],
309 degrees=True
310 )
311 fakeCat["x"] = xs
312 fakeCat["y"] = ys
313
314 return fakeCat
315
316 def _trimFakeCat(self, fakeCat, image):
317 """Trim the fake cat to the exact size of the input image.
318
319 Parameters
320 ----------
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
325 Returns
326 -------
327 fakeCat : `astropy.table.table.Table`
328 The original fakeCat trimmed to the area of the image
329 """
330
331 # fakeCat must be processed with _addPixCoords before trimming
332 fakeCat = self._addPixCoords(fakeCat, image)
333
334 # Prefilter in ra/dec to avoid cases where the wcs incorrectly maps
335 # input fakes which are really off the chip onto it.
336 ras = fakeCat["ra"] * u.deg
337 decs = fakeCat["dec"] * u.deg
338
339 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=0)
340
341 # now use the exact pixel BBox to filter to only fakes that were inserted
342 xs = fakeCat["x"]
343 ys = fakeCat["y"]
344
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
350
351 return fakeCat[isContainedRaDec & isContainedXy]
352
353
354class MatchInjectedToAssocDiaSourceConnections(
355 PipelineTaskConnections,
356 defaultTemplates={"coaddName": "deep",
357 "fakesType": "fakes_"},
358 dimensions=("instrument",
359 "visit",
360 "detector")):
361
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"),
368 )
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"),
376 )
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"),
384 )
385
386
387class MatchInjectedToAssocDiaSourceConfig(
388 PipelineTaskConfig,
389 pipelineConnections=MatchInjectedToAssocDiaSourceConnections):
390 """Config for MatchFakesTask.
391 """
392
393
394class MatchInjectedToAssocDiaSourceTask(PipelineTask):
395
396 _DefaultName = "matchInjectedToAssocDiaSource"
397 ConfigClass = MatchInjectedToAssocDiaSourceConfig
398
399 def run(self, assocDiaSources, matchDiaSources):
400 """Tag matched injected sources to associated diaSources.
401
402 Parameters
403 ----------
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``.
408 Returns
409 -------
410 result : `lsst.pipe.base.Struct`
411 Results struct with components.
412
413 - ``matchAssocDiaSources`` : Fakes matched to associated diaSources. Has
414 length of ``matchDiaSources``. (`pandas.DataFrame`)
415 """
416 # Match the fakes to the associated sources. For this we don't use the coordinates
417 # but instead check for the diaSources. Since they were present in the table already
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))
422
423 return Struct(
424 matchAssocDiaSources=matchDiaSources.merge(
425 assocDiaSources.reset_index(drop=True),
426 on="diaSourceId",
427 how="left",
428 suffixes=('_ssi', '_diaSrc')
429 )
430 )