Coverage for python/lsst/analysis/tools/tasks/photometricCatalogMatch.py: 24%

93 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-23 04:50 -0700

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__ = ("PhotometricCatalogMatchConfig", "PhotometricCatalogMatchTask") 

23 

24 

25import lsst.geom 

26import lsst.pipe.base as pipeBase 

27import numpy as np 

28from astropy.table import Table 

29from astropy.time import Time 

30from lsst.pipe.tasks.loadReferenceCatalog import LoadReferenceCatalogTask 

31 

32from ..actions.vector import StarSelector, VisitPlotFlagSelector 

33from ..tasks.catalogMatch import CatalogMatchConfig, CatalogMatchConnections, CatalogMatchTask 

34 

35 

36class PhotometricCatalogMatchConnections(CatalogMatchConnections): 

37 pass 

38 

39 

40class PhotometricCatalogMatchConfig( 

41 CatalogMatchConfig, pipelineConnections=PhotometricCatalogMatchConnections 

42): 

43 def setDefaults(self): 

44 super().setDefaults() 

45 self.matchesRefCat = True 

46 self.referenceCatalogLoader.doReferenceSelection = False 

47 self.referenceCatalogLoader.doApplyColorTerms = True 

48 

49 

50class PhotometricCatalogMatchTask(CatalogMatchTask): 

51 """A wrapper task to provide the information that 

52 is specific to the photometric reference catalog. 

53 """ 

54 

55 ConfigClass = PhotometricCatalogMatchConfig 

56 _DefaultName = "analysisToolsPhotometricCatalogMatch" 

57 

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

59 """Run the matching to the photometric reference catalog. 

60 

61 Parameters 

62 ---------- 

63 butlerQC : `lsst.pipe.base.QuantumContext` 

64 inputRefs : `lsst.pipe.base.InputQuantizedConnection` 

65 outputRefs : `lsst.pipe.base.OutputQuantizedConnection` 

66 """ 

67 

68 inputs = butlerQC.get(inputRefs) 

69 bands = [] 

70 for filterName in self.config.filterNames: 

71 bands.append(self.config.referenceCatalogLoader.refObjLoader.filterMap[filterName]) 

72 

73 # For some reason the imsim filterMaps don't work the same way as 

74 # the HSC ones do, this is a bit hacky but fixes this 

75 if bands[0].startswith("lsst") or "sim" in bands[0] or "smeared" in bands[0]: 

76 bands = self.config.filterNames 

77 

78 columns = self.prepColumns(bands) 

79 table = inputs["catalog"].get(parameters={"columns": columns}) 

80 

81 tract = butlerQC.quantum.dataId["tract"] 

82 

83 loaderTask = LoadReferenceCatalogTask( 

84 config=self.config.referenceCatalogLoader, 

85 dataIds=[ref.dataId for ref in inputRefs.refCat], 

86 name=inputs["refCat"][0].ref.datasetType.name, 

87 refCats=inputs["refCat"], 

88 ) 

89 

90 skymap = inputs.pop("skymap") 

91 loadedRefCat = self._loadRefCat(loaderTask, skymap[tract]) 

92 outputs = self.run(targetCatalog=table, refCatalog=loadedRefCat, bands=bands) 

93 

94 butlerQC.put(outputs, outputRefs) 

95 

96 

97class PhotometricCatalogMatchVisitConnections( 

98 pipeBase.PipelineTaskConnections, 

99 dimensions=("visit",), 

100 defaultTemplates={"targetCatalog": "sourceTable_visit", "refCatalog": "ps1_pv3_3pi_20170110"}, 

101): 

102 catalog = pipeBase.connectionTypes.Input( 

103 doc="The visit-wide catalog to make plots from.", 

104 storageClass="ArrowAstropy", 

105 name="{targetCatalog}", 

106 dimensions=("visit",), 

107 deferLoad=True, 

108 ) 

109 

110 refCat = pipeBase.connectionTypes.PrerequisiteInput( 

111 doc="The photometric reference catalog to match to.", 

112 name="{refCatalog}", 

113 storageClass="SimpleCatalog", 

114 dimensions=("skypix",), 

115 deferLoad=True, 

116 multiple=True, 

117 ) 

118 

119 visitSummaryTable = pipeBase.connectionTypes.Input( 

120 doc="A summary table of the ccds in the visit", 

121 storageClass="ExposureCatalog", 

122 name="visitSummary", 

123 dimensions=("visit",), 

124 ) 

125 

126 matchedCatalog = pipeBase.connectionTypes.Output( 

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

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

129 storageClass="ArrowAstropy", 

130 dimensions=("visit",), 

131 ) 

132 

133 

134class PhotometricCatalogMatchVisitConfig( 

135 PhotometricCatalogMatchConfig, pipelineConnections=PhotometricCatalogMatchVisitConnections 

136): 

137 def setDefaults(self): 

138 self.matchesRefCat = True 

139 self.filterNames = [] 

140 self.extraPerBandColumns = [] 

141 self.patchColumn = "" 

142 self.selectorBands = [] 

143 self.selectorActions.flagSelector = VisitPlotFlagSelector 

144 self.sourceSelectorActions.sourceSelector = StarSelector() 

145 self.sourceSelectorActions.sourceSelector.vectorKey = "extendedness" 

146 self.extraColumnSelectors.selector1.fluxType = "psfFlux" 

147 self.extraColumnSelectors.selector2.vectorKey = "extendedness" 

148 self.extraColumnSelectors.selector3.vectorKey = "extendedness" 

149 self.extraColumnSelectors.selector4 = VisitPlotFlagSelector 

150 

151 

152class PhotometricCatalogMatchVisitTask(PhotometricCatalogMatchTask): 

153 """A wrapper task to provide the information that 

154 is specific to the photometric reference catalog. 

155 """ 

156 

157 ConfigClass = PhotometricCatalogMatchVisitConfig 

158 _DefaultName = "analysisToolsPhotometricCatalogMatchVisit" 

159 

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

161 """Run the matching to the photometric reference catalog. 

162 

163 Parameters 

164 ---------- 

165 butlerQC : `lsst.pipe.base.QuantumContext` 

166 inputRefs : `lsst.pipe.base.InputQuantizedConnection` 

167 outputRefs : `lsst.pipe.base.OutputQuantizedConnection` 

168 """ 

169 

170 inputs = butlerQC.get(inputRefs) 

171 physicalFilter = inputs["catalog"].dataId["physical_filter"] 

172 

173 # For some reason the imsim filterMaps don't work the same way as 

174 # the HSC ones do, this is a bit hacky but fixes this 

175 if "sim" in physicalFilter: 

176 physicalFilter = physicalFilter[0] 

177 bands = [physicalFilter] 

178 else: 

179 bands = [self.config.referenceCatalogLoader.refObjLoader.filterMap[physicalFilter]] 

180 # No bands needed for visit tables 

181 # but we do need them later for the matching 

182 columns = ["coord_ra", "coord_dec", "detector"] + self.config.extraColumns.list() 

183 for selectorAction in [ 

184 self.config.selectorActions, 

185 self.config.sourceSelectorActions, 

186 self.config.extraColumnSelectors, 

187 ]: 

188 for selector in selectorAction: 

189 selectorSchema = selector.getFormattedInputSchema() 

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

191 

192 table = inputs["catalog"].get(parameters={"columns": columns}) 

193 

194 loaderTask = LoadReferenceCatalogTask( 

195 config=self.config.referenceCatalogLoader, 

196 dataIds=[ref.dataId for ref in inputRefs.refCat], 

197 name=inputs["refCat"][0].ref.datasetType.name, 

198 refCats=inputs["refCat"], 

199 ) 

200 

201 visitSummaryTable = inputs.pop("visitSummaryTable") 

202 loadedRefCat = self._loadRefCat(loaderTask, visitSummaryTable, physicalFilter) 

203 outputs = self.run(targetCatalog=table, refCatalog=loadedRefCat, bands=bands) 

204 

205 # The matcher adds the band to the front of the columns 

206 # but the visit plots aren't expecting it 

207 cols = list(outputs.matchedCatalog.columns) 

208 for col in cols: 

209 if col[:2] == bands[0] + "_": 

210 outputs.matchedCatalog.rename_column(col, col[2:]) 

211 

212 butlerQC.put(outputs, outputRefs) 

213 

214 def _loadRefCat(self, loaderTask, visitSummaryTable, physicalFilter): 

215 """Make a reference catalog with coordinates in degrees 

216 

217 Parameters 

218 ---------- 

219 visitSummaryTable : `lsst.afw.table.ExposureCatalog` 

220 The table of visit information 

221 """ 

222 # Get convex hull around the detectors, then get its center and radius 

223 corners = [] 

224 for visSum in visitSummaryTable: 

225 for ra, dec in zip(visSum["raCorners"], visSum["decCorners"]): 

226 # If the coordinates are nan then don't keep going 

227 # because it crashes later 

228 if not np.isfinite(ra) or not np.isfinite(dec): 

229 raise pipeBase.NoWorkFound("Visit summary corners not finite") 

230 corners.append(lsst.geom.SpherePoint(ra, dec, units=lsst.geom.degrees).getVector()) 

231 visitBoundingCircle = lsst.sphgeom.ConvexPolygon.convexHull(corners).getBoundingCircle() 

232 center = lsst.geom.SpherePoint(visitBoundingCircle.getCenter()) 

233 radius = visitBoundingCircle.getOpeningAngle() 

234 

235 # Get the observation date of the visit 

236 obsDate = visSum.getVisitInfo().getDate() 

237 epoch = Time(obsDate.toPython()) 

238 

239 # Load the reference catalog in the skyCircle of the detectors, then 

240 # convert the coordinates to degrees and convert the catalog to a 

241 # dataframe 

242 

243 loadedRefCat = loaderTask.getSkyCircleCatalog(center, radius, [physicalFilter], epoch=epoch) 

244 

245 return Table(loadedRefCat)