Coverage for python/lsst/analysis/tools/tasks/catalogMatch.py: 31%

99 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-10 14:10 +0000

1# This file is part of analysis_tools. 

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__ = ("CatalogMatchConfig", "CatalogMatchTask") 

23 

24 

25import lsst.geom 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28import numpy as np 

29from astropy.table import Table, hstack 

30from astropy.time import Time 

31from lsst.pipe.tasks.configurableActions import ConfigurableActionStructField 

32from lsst.pipe.tasks.loadReferenceCatalog import LoadReferenceCatalogTask 

33from lsst.skymap import BaseSkyMap 

34from smatch import Matcher 

35 

36from ..actions.vector import CoaddPlotFlagSelector, GalaxySelector, SnSelector, StarSelector 

37from ..interfaces import VectorAction 

38 

39 

40class CatalogMatchConnections( 

41 pipeBase.PipelineTaskConnections, 

42 dimensions=("tract", "skymap"), 

43 defaultTemplates={"targetCatalog": "objectTable_tract", "refCatalog": "ps1_pv3_3pi_20170110"}, 

44): 

45 catalog = pipeBase.connectionTypes.Input( 

46 doc="The tract-wide catalog to make plots from.", 

47 storageClass="ArrowAstropy", 

48 name="{targetCatalog}", 

49 dimensions=("tract", "skymap"), 

50 deferLoad=True, 

51 ) 

52 

53 refCat = pipeBase.connectionTypes.PrerequisiteInput( 

54 doc="The reference catalog to match to loaded input catalog sources.", 

55 name="{refCatalog}", 

56 storageClass="SimpleCatalog", 

57 dimensions=("skypix",), 

58 deferLoad=True, 

59 multiple=True, 

60 ) 

61 

62 skymap = pipeBase.connectionTypes.Input( 

63 doc="The skymap for the tract", 

64 storageClass="SkyMap", 

65 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

66 dimensions=("skymap",), 

67 ) 

68 

69 matchedCatalog = pipeBase.connectionTypes.Output( 

70 doc="Catalog with matched target and reference objects with separations", 

71 name="{targetCatalog}_{refCatalog}_match", 

72 storageClass="ArrowAstropy", 

73 dimensions=("tract", "skymap"), 

74 ) 

75 

76 

77class CatalogMatchConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CatalogMatchConnections): 

78 referenceCatalogLoader = pexConfig.ConfigurableField( 

79 target=LoadReferenceCatalogTask, 

80 doc="Reference catalog loader", 

81 ) 

82 

83 epoch = pexConfig.Field[float]( 

84 doc="Epoch to which reference objects are shifted.", 

85 default=2015.0, 

86 ) 

87 

88 filterNames = pexConfig.ListField[str]( 

89 doc="Physical filter names to persist downstream.", 

90 default=["u", "g", "r", "i", "z", "y"], 

91 ) 

92 

93 selectorBands = pexConfig.ListField[str]( 

94 doc="Band to use when selecting objects, primarily for extendedness.", 

95 default=["i"], 

96 ) 

97 

98 selectorActions = ConfigurableActionStructField[VectorAction]( 

99 doc="Which selectors to use to narrow down the data for QA plotting.", 

100 default={"flagSelector": CoaddPlotFlagSelector()}, 

101 ) 

102 

103 sourceSelectorActions = ConfigurableActionStructField[VectorAction]( 

104 doc="What types of sources to use.", 

105 default={"sourceSelector": StarSelector()}, 

106 ) 

107 

108 extraColumnSelectors = ConfigurableActionStructField[VectorAction]( 

109 doc="Other selectors that are not used in this task, but whose columns" "may be needed downstream", 

110 default={"selector1": SnSelector(), "selector2": GalaxySelector()}, 

111 ) 

112 

113 extraColumns = pexConfig.ListField[str]( 

114 doc="Other catalog columns to persist to downstream tasks", 

115 default=["x", "y", "patch", "ebv"], 

116 ) 

117 

118 extraPerBandColumns = pexConfig.ListField[str]( 

119 doc="Other columns to load that should be loaded for each band individually.", 

120 default=["cModelFlux"], 

121 ) 

122 

123 matchRadius = pexConfig.Field[float]( 

124 doc="The radius to use for matching, in arcsecs.", 

125 default=1.0, 

126 ) 

127 

128 raColumn = pexConfig.Field[str](doc="RA column.", default="coord_ra") 

129 decColumn = pexConfig.Field[str](doc="Dec column.", default="coord_dec") 

130 patchColumn = pexConfig.Field[str](doc="Patch column.", default="patch") 

131 

132 def setDefaults(self): 

133 super().setDefaults() 

134 self.referenceCatalogLoader.doReferenceSelection = False 

135 self.referenceCatalogLoader.doApplyColorTerms = False 

136 

137 

138class CatalogMatchTask(pipeBase.PipelineTask): 

139 """The base task for matching catalogs. Figures out which columns 

140 it needs to grab for the downstream tasks and then matches the 

141 two tables together and returns the matched and joined table 

142 including the extra columns. 

143 """ 

144 

145 ConfigClass = CatalogMatchConfig 

146 _DefaultName = "analysisToolsCatalogMatch" 

147 

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

149 """Implemented in the inherited tasks""" 

150 pass 

151 

152 def run(self, *, catalog, loadedRefCat, bands): 

153 """Takes the two catalogs and returns the matched one. 

154 

155 Parameters 

156 ---------- 

157 `catalog` : astropy.table.Table 

158 The catalog to be matched 

159 `loadedRefCat` : astropy.table.Table 

160 The loaded reference catalog 

161 `bands` : list 

162 A list of bands to apply the selectors in 

163 

164 Returns 

165 ------- 

166 `matchedCatalog` : astropy.table.Table 

167 

168 Notes 

169 ----- 

170 Performs an RA/Dec match that returns the closest match 

171 within the match radius which defaults to 1.0 arcsecond. 

172 Applies the suffix, _target, to the catalog being matched 

173 and _ref to the reference catalog. 

174 """ 

175 # Apply the selectors to the catalog 

176 mask = np.ones(len(catalog), dtype=bool) 

177 for selector in self.config.sourceSelectorActions: 

178 for band in self.config.selectorBands: 

179 mask &= selector(catalog, band=band).astype(bool) 

180 

181 targetCatalog = catalog[mask] 

182 

183 if (len(targetCatalog) == 0) or (len(loadedRefCat) == 0): 

184 refMatchIndices = np.array([], dtype=np.int64) 

185 targetMatchIndices = np.array([], dtype=np.int64) 

186 dists = np.array([], dtype=np.float64) 

187 else: 

188 # Run the matcher. 

189 

190 # This all assumes that everything is in degrees. 

191 # Which I think is okay, but the current task 

192 # allows different units. Need to configure match 

193 # radius, either in this task or a subtask. 

194 

195 # Get rid of entries in the refCat with non-finite RA/Dec values. 

196 refRas = loadedRefCat["ra"] 

197 refDecs = loadedRefCat["dec"] 

198 refRaDecFiniteMask = np.isfinite(refRas) & np.isfinite(refDecs) 

199 loadedRefCat = loadedRefCat[refRaDecFiniteMask] 

200 with Matcher(loadedRefCat["ra"], loadedRefCat["dec"]) as m: 

201 idx, refMatchIndices, targetMatchIndices, dists = m.query_radius( 

202 targetCatalog[self.config.raColumn], 

203 targetCatalog[self.config.decColumn], 

204 self.config.matchRadius / 3600.0, 

205 return_indices=True, 

206 ) 

207 

208 # Convert degrees to arcseconds. 

209 dists *= 3600.0 

210 

211 targetCatalogMatched = targetCatalog[targetMatchIndices] 

212 loadedRefCatMatched = loadedRefCat[refMatchIndices] 

213 

214 targetCols = targetCatalogMatched.columns.copy() 

215 for col in targetCols: 

216 targetCatalogMatched.rename_column(col, col + "_target") 

217 refCols = loadedRefCatMatched.columns.copy() 

218 for col in refCols: 

219 loadedRefCatMatched.rename_column(col, col + "_ref") 

220 

221 for i, band in enumerate(bands): 

222 loadedRefCatMatched[band + "_mag_ref"] = loadedRefCatMatched["refMag_ref"][:, i] 

223 loadedRefCatMatched[band + "_magErr_ref"] = loadedRefCatMatched["refMagErr_ref"][:, i] 

224 loadedRefCatMatched.remove_column("refMag_ref") 

225 loadedRefCatMatched.remove_column("refMagErr_ref") 

226 tMatched = hstack([targetCatalogMatched, loadedRefCatMatched], join_type="exact") 

227 tMatched["matchDistance"] = dists 

228 

229 return pipeBase.Struct(matchedCatalog=tMatched) 

230 

231 def prepColumns(self, bands): 

232 """Get all the columns needed for downstream tasks. 

233 Both those from the selectors and those specified in the 

234 config options. 

235 """ 

236 

237 bandColumns = [] 

238 for band in bands: 

239 for col in self.config.extraPerBandColumns: 

240 bandColumns.append(band + "_" + col) 

241 

242 columns = ( 

243 [ 

244 self.config.raColumn, 

245 self.config.decColumn, 

246 ] 

247 + self.config.extraColumns.list() 

248 + bandColumns 

249 ) 

250 

251 if self.config.patchColumn != "": 

252 columns.append(self.config.patchColumn) 

253 

254 selectorBands = list(set(list(bands) + self.config.selectorBands.list())) 

255 for selectorAction in [ 

256 self.config.selectorActions, 

257 self.config.sourceSelectorActions, 

258 self.config.extraColumnSelectors, 

259 ]: 

260 for selector in selectorAction: 

261 for band in selectorBands: 

262 selectorSchema = selector.getFormattedInputSchema(band=band) 

263 columns += [s[0] for s in selectorSchema] 

264 

265 return columns 

266 

267 def _loadRefCat(self, loaderTask, tractInfo): 

268 """Load the reference catalog that covers the 

269 catalog that is to be matched to. 

270 

271 Parameters 

272 ---------- 

273 `loaderTask` : 

274 lsst.pipe.tasks.loadReferenceCatalog.loadReferenceCatalogTask 

275 `tractInfo` : lsst.skymap.tractInfo.ExplicitTractInfo 

276 The tract information to get the sky location from 

277 

278 Returns 

279 ------- 

280 `loadedRefCat` : astropy.table.Table 

281 The reference catalog that covers the input catalog. 

282 """ 

283 boundingCircle = tractInfo.getOuterSkyPolygon().getBoundingCircle() 

284 center = lsst.geom.SpherePoint(boundingCircle.getCenter()) 

285 radius = boundingCircle.getOpeningAngle() 

286 

287 epoch = Time(self.config.epoch, format="decimalyear") 

288 

289 # This is always going to return degrees. 

290 try: 

291 loadedRefCat = loaderTask.getSkyCircleCatalog( 

292 center, radius, self.config.filterNames, epoch=epoch 

293 ) 

294 except RuntimeError as e: 

295 raise pipeBase.NoWorkFound(e) 

296 

297 return Table(loadedRefCat)