Coverage for python/lsst/faro/base/MatchedCatalogBase.py: 25%

103 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-12 02:04 -0700

1# This file is part of faro. 

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 

22import lsst.afw.table as afwTable 

23import lsst.pipe.base as pipeBase 

24import lsst.pex.config as pexConfig 

25import lsst.geom as geom 

26from lsst.utils.logging import PeriodicLogger 

27import numpy as np 

28 

29from lsst.faro.utils.matcher import matchCatalogs 

30 

31__all__ = ( 

32 "MatchedBaseConnections", 

33 "MatchedBaseConfig", 

34 "MatchedBaseTask", 

35 "MatchedTractBaseTask", 

36) 

37 

38 

39class MatchedBaseConnections( 

40 pipeBase.PipelineTaskConnections, 

41 dimensions=(), 

42 defaultTemplates={"coaddName": "deep"} 

43): 

44 sourceCatalogs = pipeBase.connectionTypes.Input( 

45 doc="Source catalogs to match up.", 

46 dimensions=("instrument", "visit", "detector", "band"), 

47 storageClass="SourceCatalog", 

48 name="src", 

49 multiple=True, 

50 ) 

51 visitSummary = pipeBase.connectionTypes.Input( 

52 doc="Exposure catalog with WCS and PhotoCalib this detector+visit combination.", 

53 dimensions=("instrument", "visit"), 

54 storageClass="ExposureCatalog", 

55 name="finalVisitSummary", 

56 multiple=True, 

57 ) 

58 skyMap = pipeBase.connectionTypes.Input( 

59 doc="Input definition of geometry/bbox and projection/wcs for warped exposures", 

60 name="skyMap", 

61 storageClass="SkyMap", 

62 dimensions=("skymap",), 

63 ) 

64 

65 

66class MatchedBaseConfig( 

67 pipeBase.PipelineTaskConfig, pipelineConnections=MatchedBaseConnections 

68): 

69 match_radius = pexConfig.Field( 

70 doc="Match radius in arcseconds.", dtype=float, default=1 

71 ) 

72 snrMin = pexConfig.Field( 

73 doc="Minimum SNR for a source to be included.", 

74 dtype=float, default=200 

75 ) 

76 snrMax = pexConfig.Field( 

77 doc="Maximum SNR for a source to be included.", 

78 dtype=float, default=np.Inf 

79 ) 

80 brightMagCut = pexConfig.Field( 

81 doc="Bright limit of catalog entries to include.", dtype=float, default=10.0 

82 ) 

83 faintMagCut = pexConfig.Field( 

84 doc="Faint limit of catalog entries to include.", dtype=float, default=30.0 

85 ) 

86 selectExtended = pexConfig.Field( 

87 doc="Whether to select extended sources", dtype=bool, default=False 

88 ) 

89 

90 

91class MatchedBaseTask(pipeBase.PipelineTask): 

92 

93 ConfigClass = MatchedBaseConfig 

94 _DefaultName = "matchedBaseTask" 

95 

96 def __init__(self, config: MatchedBaseConfig, *args, **kwargs): 

97 super().__init__(*args, config=config, **kwargs) 

98 self.radius = self.config.match_radius 

99 self.level = "patch" 

100 

101 def run( 

102 self, 

103 sourceCatalogs, 

104 photoCalibs, 

105 astromCalibs, 

106 dataIds, 

107 wcs, 

108 box, 

109 ): 

110 self.log.info("Running catalog matching") 

111 periodicLog = PeriodicLogger(self.log) 

112 radius = geom.Angle(self.radius, geom.arcseconds) 

113 if len(sourceCatalogs) < 2: 

114 self.log.warning("%s valid input catalogs: ", len(sourceCatalogs)) 

115 out_matched = afwTable.SimpleCatalog() 

116 else: 

117 srcvis, matched = matchCatalogs( 

118 sourceCatalogs, photoCalibs, astromCalibs, dataIds, radius, 

119 self.config, logger=self.log 

120 ) 

121 self.log.verbose("Finished matching catalogs.") 

122 

123 # Trim the output to the patch bounding box 

124 out_matched = type(matched)(matched.schema) 

125 self.log.info("%s sources in matched catalog.", len(matched)) 

126 for record_index, record in enumerate(matched): 

127 if box.contains(wcs.skyToPixel(record.getCoord())): 

128 out_matched.append(record) 

129 periodicLog.log("Checked %d records for trimming out of %d.", record_index + 1, len(matched)) 

130 

131 self.log.info( 

132 "%s sources when trimmed to %s boundaries.", len(out_matched), self.level 

133 ) 

134 return pipeBase.Struct(outputCatalog=out_matched) 

135 

136 def get_box_wcs(self, skymap, oid): 

137 tract_info = skymap.generateTract(oid["tract"]) 

138 wcs = tract_info.getWcs() 

139 patch_info = tract_info.getPatchInfo(oid["patch"]) 

140 patch_box = patch_info.getInnerBBox() 

141 self.log.info("Running tract: %s and patch: %s", oid["tract"], oid["patch"]) 

142 return patch_box, wcs 

143 

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

145 inputs = butlerQC.get(inputRefs) 

146 self.log.verbose("Inputs obtained from the butler.") 

147 oid = dict(outputRefs.outputCatalog.dataId.required) 

148 skymap = inputs["skyMap"] 

149 del inputs["skyMap"] 

150 box, wcs = self.get_box_wcs(skymap, oid) 

151 # Cast to float to handle fractional pixels 

152 box = geom.Box2D(box) 

153 inputs["dataIds"] = [el.dataId for el in inputRefs.sourceCatalogs] 

154 inputs["wcs"] = wcs 

155 inputs["box"] = box 

156 visitSummary = inputs.pop("visitSummary") 

157 

158 flatVisitSummaryList = np.hstack(visitSummary) 

159 visitSummaryVisitIdList = np.array( 

160 [calib["visit"] for calib in flatVisitSummaryList] 

161 ) 

162 visitSummaryDetectorIdList = np.array( 

163 [calib["id"] for calib in flatVisitSummaryList] 

164 ) 

165 

166 remove_indices = [] 

167 inputs["photoCalibs"] = [None] * len(inputs["dataIds"]) 

168 inputs["astromCalibs"] = [None] * len(inputs["dataIds"]) 

169 

170 for i in range(len(inputs["dataIds"])): 

171 dataId = inputs["dataIds"][i] 

172 detector = dataId["detector"] 

173 visit = dataId["visit"] 

174 row_find = (visitSummaryVisitIdList == visit) & ( 

175 visitSummaryDetectorIdList == detector 

176 ) 

177 if np.sum(row_find) < 1: 

178 self.log.warning("Detector id %s not found in visit summary " 

179 "for visit %s and will not be used.", 

180 detector, visit) 

181 inputs["photoCalibs"][i] = None 

182 inputs["astromCalibs"][i] = None 

183 remove_indices.append(i) 

184 else: 

185 row = flatVisitSummaryList[row_find] 

186 inputs["photoCalibs"][i] = row[0].getPhotoCalib() 

187 inputs["astromCalibs"][i] = row[0].getWcs() 

188 

189 # Remove datasets that didn't have matching external calibs 

190 remove_indices = np.unique(np.array(remove_indices)) 

191 if len(remove_indices) > 0: 

192 for ind in sorted(remove_indices, reverse=True): 

193 del inputs['sourceCatalogs'][ind] 

194 del inputs['dataIds'][ind] 

195 del inputs['photoCalibs'][ind] 

196 del inputs['astromCalibs'][ind] 

197 

198 outputs = self.run(**inputs) 

199 butlerQC.put(outputs, outputRefs) 

200 

201 

202class MatchedTractBaseTask(MatchedBaseTask): 

203 

204 ConfigClass = MatchedBaseConfig 

205 _DefaultName = "matchedTractBaseTask" 

206 

207 def __init__(self, config: MatchedBaseConfig, *args, **kwargs): 

208 super().__init__(*args, config=config, **kwargs) 

209 self.radius = self.config.match_radius 

210 self.level = "tract" 

211 

212 def get_box_wcs(self, skymap, oid): 

213 tract_info = skymap.generateTract(oid["tract"]) 

214 wcs = tract_info.getWcs() 

215 tract_box = tract_info.getBBox() 

216 self.log.info("Running tract: %s", oid["tract"]) 

217 return tract_box, wcs