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

95 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 13:17 +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 with Matcher(loadedRefCat["ra"], loadedRefCat["dec"]) as m: 

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

196 targetCatalog[self.config.raColumn], 

197 targetCatalog[self.config.decColumn], 

198 self.config.matchRadius / 3600.0, 

199 return_indices=True, 

200 ) 

201 

202 # Convert degrees to arcseconds. 

203 dists *= 3600.0 

204 

205 targetCatalogMatched = targetCatalog[targetMatchIndices] 

206 loadedRefCatMatched = loadedRefCat[refMatchIndices] 

207 

208 targetCols = targetCatalogMatched.columns.copy() 

209 for col in targetCols: 

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

211 refCols = loadedRefCatMatched.columns.copy() 

212 for col in refCols: 

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

214 

215 for i, band in enumerate(bands): 

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

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

218 loadedRefCatMatched.remove_column("refMag_ref") 

219 loadedRefCatMatched.remove_column("refMagErr_ref") 

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

221 tMatched["matchDistance"] = dists 

222 

223 return pipeBase.Struct(matchedCatalog=tMatched) 

224 

225 def prepColumns(self, bands): 

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

227 Both those from the selectors and those specified in the 

228 config options. 

229 """ 

230 

231 bandColumns = [] 

232 for band in bands: 

233 for col in self.config.extraPerBandColumns: 

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

235 

236 columns = ( 

237 [ 

238 self.config.raColumn, 

239 self.config.decColumn, 

240 ] 

241 + self.config.extraColumns.list() 

242 + bandColumns 

243 ) 

244 

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

246 columns.append(self.config.patchColumn) 

247 

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

249 for selectorAction in [ 

250 self.config.selectorActions, 

251 self.config.sourceSelectorActions, 

252 self.config.extraColumnSelectors, 

253 ]: 

254 for selector in selectorAction: 

255 for band in selectorBands: 

256 selectorSchema = selector.getFormattedInputSchema(band=band) 

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

258 

259 return columns 

260 

261 def _loadRefCat(self, loaderTask, tractInfo): 

262 """Load the reference catalog that covers the 

263 catalog that is to be matched to. 

264 

265 Parameters 

266 ---------- 

267 `loaderTask` : 

268 lsst.pipe.tasks.loadReferenceCatalog.loadReferenceCatalogTask 

269 `tractInfo` : lsst.skymap.tractInfo.ExplicitTractInfo 

270 The tract information to get the sky location from 

271 

272 Returns 

273 ------- 

274 `loadedRefCat` : astropy.table.Table 

275 The reference catalog that covers the input catalog. 

276 """ 

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

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

279 radius = boundingCircle.getOpeningAngle() 

280 

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

282 

283 # This is always going to return degrees. 

284 try: 

285 loadedRefCat = loaderTask.getSkyCircleCatalog( 

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

287 ) 

288 except RuntimeError as e: 

289 raise pipeBase.NoWorkFound(e) 

290 

291 return Table(loadedRefCat)