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

84 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-09 04:18 -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__ = ( 

23 "AstrometricCatalogMatchConfig", 

24 "AstrometricCatalogMatchTask", 

25 "AstrometricCatalogMatchVisitConfig", 

26 "AstrometricCatalogMatchVisitTask", 

27) 

28 

29import lsst.geom 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32import numpy as np 

33from astropy.table import Table 

34from astropy.time import Time 

35from lsst.pex.config.configurableActions import ConfigurableActionStructField 

36from lsst.pipe.tasks.loadReferenceCatalog import LoadReferenceCatalogTask 

37 

38from ..actions.vector import StarSelector, VisitPlotFlagSelector 

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

40 

41 

42class AstrometricCatalogMatchConfig(CatalogMatchConfig, pipelineConnections=CatalogMatchConnections): 

43 bands = pexConfig.ListField[str]( 

44 doc="The bands to persist downstream", 

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

46 ) 

47 

48 def setDefaults(self): 

49 super().setDefaults() 

50 self.matchesRefCat = True 

51 self.referenceCatalogLoader.doApplyColorTerms = False 

52 self.referenceCatalogLoader.refObjLoader.requireProperMotion = True 

53 self.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis = "phot_g_mean" 

54 self.connections.refCatalog = "gaia_dr3_20230707" 

55 

56 

57class AstrometricCatalogMatchTask(CatalogMatchTask): 

58 """Match a tract-level catalog to a reference catalog""" 

59 

60 ConfigClass = AstrometricCatalogMatchConfig 

61 _DefaultName = "analysisToolsAstrometricCatalogMatch" 

62 

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

64 # Docs inherited from base class 

65 

66 inputs = butlerQC.get(inputRefs) 

67 columns = self.prepColumns(self.config.bands) 

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

69 

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

71 

72 loaderTask = LoadReferenceCatalogTask( 

73 config=self.config.referenceCatalogLoader, 

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

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

76 refCats=inputs["refCat"], 

77 ) 

78 

79 skymap = inputs.pop("skymap") 

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

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

82 

83 butlerQC.put(outputs, outputRefs) 

84 

85 

86class AstrometricCatalogMatchVisitConnections( 

87 pipeBase.PipelineTaskConnections, 

88 dimensions=("visit",), 

89 defaultTemplates={"targetCatalog": "sourceTable_visit", "refCatalog": "gaia_dr3_20230707"}, 

90): 

91 catalog = pipeBase.connectionTypes.Input( 

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

93 storageClass="ArrowAstropy", 

94 name="{targetCatalog}", 

95 dimensions=("visit",), 

96 deferLoad=True, 

97 ) 

98 

99 refCat = pipeBase.connectionTypes.PrerequisiteInput( 

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

101 name="{refCatalog}", 

102 storageClass="SimpleCatalog", 

103 dimensions=("skypix",), 

104 deferLoad=True, 

105 multiple=True, 

106 ) 

107 

108 visitSummaryTable = pipeBase.connectionTypes.Input( 

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

110 storageClass="ExposureCatalog", 

111 name="visitSummary", 

112 dimensions=("visit",), 

113 ) 

114 

115 matchedCatalog = pipeBase.connectionTypes.Output( 

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

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

118 storageClass="ArrowAstropy", 

119 dimensions=("visit",), 

120 ) 

121 

122 

123class AstrometricCatalogMatchVisitConfig( 

124 AstrometricCatalogMatchConfig, pipelineConnections=AstrometricCatalogMatchVisitConnections 

125): 

126 selectorActions = ConfigurableActionStructField( 

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

128 default={"flagSelector": VisitPlotFlagSelector}, 

129 ) 

130 

131 extraColumns = pexConfig.ListField[str]( 

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

133 default=["psfFlux", "psfFluxErr"], 

134 ) 

135 

136 bands = pexConfig.ListField[str]( 

137 doc="The bands to persist downstream", 

138 default=[], 

139 ) 

140 

141 def setDefaults(self): 

142 self.matchesRefCat = True 

143 # sourceSelectorActions.sourceSelector is StarSelector 

144 self.sourceSelectorActions.sourceSelector = StarSelector() 

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

146 # extraColumnSelectors.selector1 is SnSelector 

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

148 # extraColumnSelectors.selector2 is GalaxySelector 

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

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

151 self.extraColumnSelectors.selector4 = VisitPlotFlagSelector 

152 self.referenceCatalogLoader.doApplyColorTerms = False 

153 self.referenceCatalogLoader.refObjLoader.requireProperMotion = False 

154 self.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis = "phot_g_mean" 

155 

156 

157class AstrometricCatalogMatchVisitTask(AstrometricCatalogMatchTask): 

158 """Match a visit-level catalog to a reference catalog""" 

159 

160 ConfigClass = AstrometricCatalogMatchVisitConfig 

161 _DefaultName = "analysisToolsAstrometricCatalogMatchVisit" 

162 

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

164 # Docs inherited from base class 

165 

166 inputs = butlerQC.get(inputRefs) 

167 

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

169 for selectorAction in [ 

170 self.config.selectorActions, 

171 self.config.sourceSelectorActions, 

172 self.config.extraColumnSelectors, 

173 ]: 

174 for selector in selectorAction: 

175 selectorSchema = selector.getFormattedInputSchema() 

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

177 

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

179 

180 loaderTask = LoadReferenceCatalogTask( 

181 config=self.config.referenceCatalogLoader, 

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

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

184 refCats=inputs["refCat"], 

185 ) 

186 

187 visitSummaryTable = inputs.pop("visitSummaryTable") 

188 loadedRefCat = self._loadRefCat(loaderTask, visitSummaryTable) 

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

190 

191 butlerQC.put(outputs, outputRefs) 

192 

193 def _loadRefCat(self, loaderTask, visitSummaryTable): 

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

195 

196 Parameters 

197 ---------- 

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

199 The table of visit information 

200 """ 

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

202 corners = [] 

203 for visSum in visitSummaryTable: 

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

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

206 # because it crashes later 

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

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

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

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

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

212 radius = visitBoundingCircle.getOpeningAngle() 

213 

214 # Get the observation date of the visit 

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

216 epoch = Time(obsDate.toPython()) 

217 

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

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

220 # dataframe 

221 

222 filterName = self.config.referenceCatalogLoader.refObjLoader.anyFilterMapsToThis 

223 loadedRefCat = loaderTask.getSkyCircleCatalog(center, radius, filterName, epoch=epoch) 

224 

225 return Table(loadedRefCat)