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

83 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-30 14:25 +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__ = ("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 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.referenceCatalogLoader.doReferenceSelection = False 

46 self.referenceCatalogLoader.doApplyColorTerms = True 

47 

48 

49class PhotometricCatalogMatchTask(CatalogMatchTask): 

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

51 is specific to the photometric reference catalog. 

52 """ 

53 

54 ConfigClass = PhotometricCatalogMatchConfig 

55 _DefaultName = "analysisToolsPhotometricCatalogMatch" 

56 

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

58 """Run the matching to the photometric reference 

59 catalog. 

60 

61 Parameters 

62 ---------- 

63 `butlerQC` : lsst.pipe.base.butlerQuantumContext.ButlerQuantumContext 

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

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

66 

67 """ 

68 

69 inputs = butlerQC.get(inputRefs) 

70 bands = [] 

71 for filterName in self.config.filterNames: 

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

73 

74 columns = self.prepColumns(bands) 

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

76 

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

78 

79 loaderTask = LoadReferenceCatalogTask( 

80 config=self.config.referenceCatalogLoader, 

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

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

83 refCats=inputs["refCat"], 

84 ) 

85 

86 skymap = inputs.pop("skymap") 

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

88 outputs = self.run(catalog=table, loadedRefCat=loadedRefCat, bands=bands) 

89 

90 butlerQC.put(outputs, outputRefs) 

91 

92 

93class PhotometricCatalogMatchVisitConnections( 

94 pipeBase.PipelineTaskConnections, 

95 dimensions=("visit",), 

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

97): 

98 catalog = pipeBase.connectionTypes.Input( 

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

100 storageClass="ArrowAstropy", 

101 name="{targetCatalog}", 

102 dimensions=("visit",), 

103 deferLoad=True, 

104 ) 

105 

106 refCat = pipeBase.connectionTypes.PrerequisiteInput( 

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

108 name="{refCatalog}", 

109 storageClass="SimpleCatalog", 

110 dimensions=("skypix",), 

111 deferLoad=True, 

112 multiple=True, 

113 ) 

114 

115 visitSummaryTable = pipeBase.connectionTypes.Input( 

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

117 storageClass="ExposureCatalog", 

118 name="visitSummary", 

119 dimensions=("visit",), 

120 ) 

121 

122 matchedCatalog = pipeBase.connectionTypes.Output( 

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

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

125 storageClass="ArrowAstropy", 

126 dimensions=("visit",), 

127 ) 

128 

129 

130class PhotometricCatalogMatchVisitConfig( 

131 PhotometricCatalogMatchConfig, pipelineConnections=PhotometricCatalogMatchVisitConnections 

132): 

133 def setDefaults(self): 

134 self.filterNames = [] 

135 self.extraPerBandColumns = [] 

136 self.patchColumn = "" 

137 self.selectorBands = [] 

138 self.selectorActions.flagSelector = VisitPlotFlagSelector 

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

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

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

142 

143 

144class PhotometricCatalogMatchVisitTask(PhotometricCatalogMatchTask): 

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

146 is specific to the photometric reference catalog. 

147 """ 

148 

149 ConfigClass = PhotometricCatalogMatchVisitConfig 

150 _DefaultName = "analysisToolsPhotometricCatalogMatchVisit" 

151 

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

153 """Run the matching to the photometric reference 

154 catalog. 

155 

156 Parameters 

157 ---------- 

158 `butlerQC` : lsst.pipe.base.butlerQuantumContext.ButlerQuantumContext 

159 `inputRefs` : lsst.pipe.base.connections.InputQuantizedConnection 

160 `outputRefs` : lsst.pipe.base.connections.OutputQuantizedConnection 

161 

162 """ 

163 

164 inputs = butlerQC.get(inputRefs) 

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

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

167 # No bands needed for visit tables 

168 # but we do need them later for the matching 

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

170 for selectorAction in [ 

171 self.config.selectorActions, 

172 self.config.sourceSelectorActions, 

173 self.config.extraColumnSelectors, 

174 ]: 

175 for selector in selectorAction: 

176 selectorSchema = selector.getFormattedInputSchema() 

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

178 

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

180 

181 loaderTask = LoadReferenceCatalogTask( 

182 config=self.config.referenceCatalogLoader, 

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

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

185 refCats=inputs["refCat"], 

186 ) 

187 

188 visitSummaryTable = inputs.pop("visitSummaryTable") 

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

190 outputs = self.run(catalog=table, loadedRefCat=loadedRefCat, bands=bands) 

191 

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

193 # but the visit plots aren't expecting it 

194 cols = list(outputs.matchedCatalog.columns) 

195 for col in cols: 

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

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

198 

199 butlerQC.put(outputs, outputRefs) 

200 

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

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

203 

204 Parameters 

205 ---------- 

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

207 The table of visit information 

208 """ 

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

210 corners = [] 

211 for visSum in visitSummaryTable: 

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

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

214 # because it crashes later 

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

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

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

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

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

220 radius = visitBoundingCircle.getOpeningAngle() 

221 

222 # Get the observation date of the visit 

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

224 epoch = Time(obsDate.toPython()) 

225 

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

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

228 # dataframe 

229 

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

231 

232 return Table(loadedRefCat)