Coverage for python/lsst/pipe/tasks/matchFakes.py: 43%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

76 statements  

1# This file is part of pipe_tasks. 

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 

22import astropy.units as u 

23import numpy as np 

24from scipy.spatial import cKDTree 

25 

26from lsst.geom import Box2D, radians, SpherePoint 

27import lsst.pex.config as pexConfig 

28from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct 

29import lsst.pipe.base.connectionTypes as connTypes 

30from lsst.pipe.tasks.insertFakes import InsertFakesConfig 

31 

32__all__ = ["MatchFakesTask", 

33 "MatchFakesConfig", 

34 "MatchVariableFakesConfig", 

35 "MatchVariableFakesTask"] 

36 

37 

38class MatchFakesConnections(PipelineTaskConnections, 

39 defaultTemplates={"coaddName": "deep", 

40 "fakesType": "fakes_"}, 

41 dimensions=("tract", 

42 "skymap", 

43 "instrument", 

44 "visit", 

45 "detector")): 

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") 

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 associatedDiaSources = connTypes.Input( 

59 doc="A DiaSource catalog to match against fakeCat. Assumed " 

60 "to be SDMified.", 

61 name="{fakesType}{coaddName}Diff_assocDiaSrc", 

62 storageClass="DataFrame", 

63 dimensions=("instrument", "visit", "detector"), 

64 ) 

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"), 

72 ) 

73 

74 

75class MatchFakesConfig( 

76 InsertFakesConfig, 

77 pipelineConnections=MatchFakesConnections): 

78 """Config for MatchFakesTask. 

79 """ 

80 matchDistanceArcseconds = pexConfig.RangeField( 

81 doc="Distance in arcseconds to match within.", 

82 dtype=float, 

83 default=0.5, 

84 min=0, 

85 max=10, 

86 ) 

87 

88 

89class MatchFakesTask(PipelineTask): 

90 """Match a pre-existing catalog of fakes to a catalog of detections on 

91 a difference image. 

92 

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. 

96 """ 

97 

98 _DefaultName = "matchFakes" 

99 ConfigClass = MatchFakesConfig 

100 

101 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

102 inputs = butlerQC.get(inputRefs) 

103 

104 outputs = self.run(**inputs) 

105 butlerQC.put(outputs, outputRefs) 

106 

107 def run(self, fakeCat, diffIm, associatedDiaSources): 

108 """Match fakes to detected diaSources within a difference image bound. 

109 

110 Parameters 

111 ---------- 

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``. 

118 

119 Returns 

120 ------- 

121 result : `lsst.pipe.base.Struct` 

122 Results struct with components. 

123 

124 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has 

125 length of ``fakeCat``. (`pandas.DataFrame`) 

126 """ 

127 trimmedFakes = self._trimFakeCat(fakeCat, diffIm) 

128 nPossibleFakes = len(trimmedFakes) 

129 

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"])) 

135 

136 diaSrcTree = cKDTree(diaSrcVects) 

137 dist, idxs = diaSrcTree.query( 

138 fakeVects, 

139 distance_upper_bound=np.radians(self.config.matchDistanceArcseconds / 3600)) 

140 nFakesFound = np.isfinite(dist).sum() 

141 

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)) 

145 

146 return Struct( 

147 matchedDiaSources=matchedFakes.merge( 

148 associatedDiaSources.reset_index(drop=True), on="diaSourceId", how="left") 

149 ) 

150 

151 def _trimFakeCat(self, fakeCat, image): 

152 """Trim the fake cat to about the size of the input image. 

153 

154 Parameters 

155 ---------- 

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 

160 

161 Returns 

162 ------- 

163 fakeCat : `pandas.core.frame.DataFrame` 

164 The original fakeCat trimmed to the area of the image 

165 """ 

166 wcs = image.getWcs() 

167 

168 bbox = Box2D(image.getBBox()) 

169 

170 def trim(row): 

171 coord = SpherePoint(row[self.config.raColName], 

172 row[self.config.decColName], 

173 radians) 

174 cent = wcs.skyToPixel(coord) 

175 return bbox.contains(cent) 

176 

177 return fakeCat[fakeCat.apply(trim, axis=1)] 

178 

179 def _getVectors(self, ras, decs): 

180 """Convert ra dec to unit vectors on the sphere. 

181 

182 Parameters 

183 ---------- 

184 ras : `numpy.ndarray`, (N,) 

185 RA coordinates in radians. 

186 decs : `numpy.ndarray`, (N,) 

187 Dec coordinates in radians. 

188 

189 Returns 

190 ------- 

191 vectors : `numpy.ndarray`, (N, 3) 

192 Vectors on the unit sphere for the given RA/DEC values. 

193 """ 

194 vectors = np.empty((len(ras), 3)) 

195 

196 vectors[:, 2] = np.sin(decs) 

197 vectors[:, 0] = np.cos(decs) * np.cos(ras) 

198 vectors[:, 1] = np.cos(decs) * np.sin(ras) 

199 

200 return vectors 

201 

202 

203class MatchVariableFakesConnections(MatchFakesConnections): 

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"), 

209 ) 

210 

211 

212class MatchVariableFakesConfig(MatchFakesConfig, 

213 pipelineConnections=MatchVariableFakesConnections): 

214 """Config for MatchFakesTask. 

215 """ 

216 pass 

217 

218 

219class 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 

222 subtraction. 

223 

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. 

227 """ 

228 _DefaultName = "matchVariableFakes" 

229 ConfigClass = MatchVariableFakesConfig 

230 

231 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

232 inputs = butlerQC.get(inputRefs) 

233 inputs["band"] = butlerQC.quantum.dataId["band"] 

234 

235 outputs = self.run(**inputs) 

236 butlerQC.put(outputs, outputRefs) 

237 

238 def run(self, fakeCat, ccdVisitFakeMagnitudes, diffIm, associatedDiaSources, band): 

239 """Match fakes to detected diaSources within a difference image bound. 

240 

241 Parameters 

242 ---------- 

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``. 

249 

250 Returns 

251 ------- 

252 result : `lsst.pipe.base.Struct` 

253 Results struct with components. 

254 

255 - ``matchedDiaSources`` : Fakes matched to input diaSources. Has 

256 length of ``fakeCat``. (`pandas.DataFrame`) 

257 """ 

258 self.computeExpectedDiffMag(fakeCat, ccdVisitFakeMagnitudes, band) 

259 return super().run(fakeCat, diffIm, associatedDiaSources) 

260 

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. 

264 

265 Negative magnitudes indicate that the source should be detected as 

266 a negative source. 

267 

268 Parameters 

269 ---------- 

270 fakeCat : `pandas.DataFrame` 

271 Catalog of fake sources. 

272 ccdVisitFakeMagnitudes : `pandas.DataFrame` 

273 Magnitudes for variable sources in this specific ccdVisit. 

274 band : `str` 

275 Band that this ccdVisit was observed in. 

276 """ 

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)) 

284 

285 noVisit = ~fakeCat["isVisitSource"] 

286 noTemplate = ~fakeCat["isTemplateSource"] 

287 both = np.logical_and(fakeCat["isVisitSource"], 

288 fakeCat["isTemplateSource"]) 

289 

290 fakeCat.loc[noVisit, magName] = -magnitudes[noVisit] 

291 fakeCat.loc[noTemplate, magName] = visitMags[noTemplate] 

292 fakeCat.loc[both, magName] = diffMag[both]