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

83 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-22 11:06 +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__ = ( 

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 

55 

56class AstrometricCatalogMatchTask(CatalogMatchTask): 

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

58 

59 ConfigClass = AstrometricCatalogMatchConfig 

60 _DefaultName = "analysisToolsAstrometricCatalogMatch" 

61 

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

63 # Docs inherited from base class 

64 

65 inputs = butlerQC.get(inputRefs) 

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

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

68 

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

70 

71 loaderTask = LoadReferenceCatalogTask( 

72 config=self.config.referenceCatalogLoader, 

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

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

75 refCats=inputs["refCat"], 

76 ) 

77 

78 skymap = inputs.pop("skymap") 

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

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

81 

82 butlerQC.put(outputs, outputRefs) 

83 

84 

85class AstrometricCatalogMatchVisitConnections( 

86 pipeBase.PipelineTaskConnections, 

87 dimensions=("visit",), 

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

89): 

90 catalog = pipeBase.connectionTypes.Input( 

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

92 storageClass="ArrowAstropy", 

93 name="{targetCatalog}", 

94 dimensions=("visit",), 

95 deferLoad=True, 

96 ) 

97 

98 refCat = pipeBase.connectionTypes.PrerequisiteInput( 

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

100 name="{refCatalog}", 

101 storageClass="SimpleCatalog", 

102 dimensions=("skypix",), 

103 deferLoad=True, 

104 multiple=True, 

105 ) 

106 

107 visitSummaryTable = pipeBase.connectionTypes.Input( 

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

109 storageClass="ExposureCatalog", 

110 name="visitSummary", 

111 dimensions=("visit",), 

112 ) 

113 

114 matchedCatalog = pipeBase.connectionTypes.Output( 

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

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

117 storageClass="ArrowAstropy", 

118 dimensions=("visit",), 

119 ) 

120 

121 

122class AstrometricCatalogMatchVisitConfig( 

123 AstrometricCatalogMatchConfig, pipelineConnections=AstrometricCatalogMatchVisitConnections 

124): 

125 selectorActions = ConfigurableActionStructField( 

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

127 default={"flagSelector": VisitPlotFlagSelector}, 

128 ) 

129 

130 extraColumns = pexConfig.ListField[str]( 

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

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

133 ) 

134 

135 bands = pexConfig.ListField[str]( 

136 doc="The bands to persist downstream", 

137 default=[], 

138 ) 

139 

140 def setDefaults(self): 

141 self.matchesRefCat = True 

142 # sourceSelectorActions.sourceSelector is StarSelector 

143 self.sourceSelectorActions.sourceSelector = StarSelector() 

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

145 # extraColumnSelectors.selector1 is SnSelector 

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

147 # extraColumnSelectors.selector2 is GalaxySelector 

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

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

150 self.extraColumnSelectors.selector4 = VisitPlotFlagSelector 

151 self.referenceCatalogLoader.doApplyColorTerms = False 

152 self.referenceCatalogLoader.refObjLoader.requireProperMotion = False 

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

154 

155 

156class AstrometricCatalogMatchVisitTask(AstrometricCatalogMatchTask): 

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

158 

159 ConfigClass = AstrometricCatalogMatchVisitConfig 

160 _DefaultName = "analysisToolsAstrometricCatalogMatchVisit" 

161 

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

163 # Docs inherited from base class 

164 

165 inputs = butlerQC.get(inputRefs) 

166 

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

168 for selectorAction in [ 

169 self.config.selectorActions, 

170 self.config.sourceSelectorActions, 

171 self.config.extraColumnSelectors, 

172 ]: 

173 for selector in selectorAction: 

174 selectorSchema = selector.getFormattedInputSchema() 

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

176 

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

178 

179 loaderTask = LoadReferenceCatalogTask( 

180 config=self.config.referenceCatalogLoader, 

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

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

183 refCats=inputs["refCat"], 

184 ) 

185 

186 visitSummaryTable = inputs.pop("visitSummaryTable") 

187 loadedRefCat = self._loadRefCat(loaderTask, visitSummaryTable) 

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

189 

190 butlerQC.put(outputs, outputRefs) 

191 

192 def _loadRefCat(self, loaderTask, visitSummaryTable): 

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

194 

195 Parameters 

196 ---------- 

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

198 The table of visit information 

199 """ 

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

201 corners = [] 

202 for visSum in visitSummaryTable: 

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

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

205 # because it crashes later 

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

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

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

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

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

211 radius = visitBoundingCircle.getOpeningAngle() 

212 

213 # Get the observation date of the visit 

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

215 epoch = Time(obsDate.toPython()) 

216 

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

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

219 # dataframe 

220 

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

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

223 

224 return Table(loadedRefCat)