Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import lsst.pipe.base as pipeBase 

2import lsst.pex.config as pexConfig 

3import lsst.geom as geom 

4import numpy as np 

5 

6from lsst.faro.utils.matcher import matchCatalogs 

7 

8__all__ = ( 

9 "MatchedBaseConnections", 

10 "MatchedBaseConfig", 

11 "MatchedBaseTask", 

12 "MatchedTractBaseTask", 

13) 

14 

15 

16class MatchedBaseConnections( 

17 pipeBase.PipelineTaskConnections, 

18 dimensions=(), 

19 defaultTemplates={ 

20 "coaddName": "deep", 

21 "photoCalibName": "calexp.photoCalib", 

22 "wcsName": "calexp.wcs", 

23 "externalPhotoCalibName": "fgcm", 

24 "externalWcsName": "jointcal", 

25 }, 

26): 

27 sourceCatalogs = pipeBase.connectionTypes.Input( 

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

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

30 storageClass="SourceCatalog", 

31 name="src", 

32 multiple=True, 

33 ) 

34 photoCalibs = pipeBase.connectionTypes.Input( 

35 doc="Photometric calibration object.", 

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

37 storageClass="PhotoCalib", 

38 name="{photoCalibName}", 

39 multiple=True, 

40 ) 

41 astromCalibs = pipeBase.connectionTypes.Input( 

42 doc="WCS for the catalog.", 

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

44 storageClass="Wcs", 

45 name="{wcsName}", 

46 multiple=True, 

47 ) 

48 externalSkyWcsTractCatalog = pipeBase.connectionTypes.Input( 

49 doc=( 

50 "Per-tract, per-visit wcs calibrations. These catalogs use the detector " 

51 "id for the catalog id, sorted on id for fast lookup." 

52 ), 

53 name="{externalWcsName}SkyWcsCatalog", 

54 storageClass="ExposureCatalog", 

55 dimensions=("instrument", "visit", "tract", "band"), 

56 multiple=True, 

57 ) 

58 externalSkyWcsGlobalCatalog = pipeBase.connectionTypes.Input( 

59 doc=( 

60 "Per-visit wcs calibrations computed globally (with no tract information). " 

61 "These catalogs use the detector id for the catalog id, sorted on id for " 

62 "fast lookup." 

63 ), 

64 name="{externalWcsName}SkyWcsCatalog", 

65 storageClass="ExposureCatalog", 

66 dimensions=("instrument", "visit", "band"), 

67 multiple=True, 

68 ) 

69 externalPhotoCalibTractCatalog = pipeBase.connectionTypes.Input( 

70 doc=( 

71 "Per-tract, per-visit photometric calibrations. These catalogs use the " 

72 "detector id for the catalog id, sorted on id for fast lookup." 

73 ), 

74 name="{externalPhotoCalibName}PhotoCalibCatalog", 

75 storageClass="ExposureCatalog", 

76 dimensions=("instrument", "visit", "tract", "band"), 

77 multiple=True, 

78 ) 

79 externalPhotoCalibGlobalCatalog = pipeBase.connectionTypes.Input( 

80 doc=( 

81 "Per-visit photometric calibrations computed globally (with no tract " 

82 "information). These catalogs use the detector id for the catalog id, " 

83 "sorted on id for fast lookup." 

84 ), 

85 name="{externalPhotoCalibName}PhotoCalibCatalog", 

86 storageClass="ExposureCatalog", 

87 dimensions=("instrument", "visit", "band"), 

88 multiple=True, 

89 ) 

90 skyMap = pipeBase.connectionTypes.Input( 

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

92 name="skyMap", 

93 storageClass="SkyMap", 

94 dimensions=("skymap",), 

95 ) 

96 

97 def __init__(self, *, config=None): 

98 super().__init__(config=config) 

99 if config.doApplyExternalSkyWcs: 

100 if config.useGlobalExternalSkyWcs: 

101 self.inputs.remove("externalSkyWcsTractCatalog") 

102 else: 

103 self.inputs.remove("externalSkyWcsGlobalCatalog") 

104 else: 

105 self.inputs.remove("externalSkyWcsTractCatalog") 

106 self.inputs.remove("externalSkyWcsGlobalCatalog") 

107 if config.doApplyExternalPhotoCalib: 

108 if config.useGlobalExternalPhotoCalib: 

109 self.inputs.remove("externalPhotoCalibTractCatalog") 

110 else: 

111 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

112 else: 

113 self.inputs.remove("externalPhotoCalibTractCatalog") 

114 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

115 

116 

117class MatchedBaseConfig( 

118 pipeBase.PipelineTaskConfig, pipelineConnections=MatchedBaseConnections 

119): 

120 match_radius = pexConfig.Field( 

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

122 ) 

123 doApplyExternalSkyWcs = pexConfig.Field( 

124 doc="Whether or not to use the external wcs.", dtype=bool, default=False 

125 ) 

126 useGlobalExternalSkyWcs = pexConfig.Field( 

127 doc="Whether or not to use the global external wcs.", dtype=bool, default=False 

128 ) 

129 doApplyExternalPhotoCalib = pexConfig.Field( 

130 doc="Whether or not to use the external photoCalib.", dtype=bool, default=False 

131 ) 

132 useGlobalExternalPhotoCalib = pexConfig.Field( 

133 doc="Whether or not to use the global external photoCalib.", 

134 dtype=bool, 

135 default=False, 

136 ) 

137 

138 

139class MatchedBaseTask(pipeBase.PipelineTask): 

140 

141 ConfigClass = MatchedBaseConfig 

142 _DefaultName = "matchedBaseTask" 

143 

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

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

146 self.radius = self.config.match_radius 

147 self.level = "patch" 

148 

149 def run( 

150 self, 

151 sourceCatalogs, 

152 photoCalibs, 

153 astromCalibs, 

154 dataIds, 

155 wcs, 

156 box, 

157 doApplyExternalSkyWcs=False, 

158 doApplyExternalPhotoCalib=False, 

159 ): 

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

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

162 srcvis, matched = matchCatalogs( 

163 sourceCatalogs, photoCalibs, astromCalibs, dataIds, radius, logger=self.log 

164 ) 

165 # Trim the output to the patch bounding box 

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

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

168 for record in matched: 

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

170 out_matched.append(record) 

171 self.log.info( 

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

173 ) 

174 return pipeBase.Struct(outputCatalog=out_matched) 

175 

176 def get_box_wcs(self, skymap, oid): 

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

178 wcs = tract_info.getWcs() 

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

180 patch_box = patch_info.getInnerBBox() 

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

182 return patch_box, wcs 

183 

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

185 inputs = butlerQC.get(inputRefs) 

186 oid = outputRefs.outputCatalog.dataId.byName() 

187 skymap = inputs["skyMap"] 

188 del inputs["skyMap"] 

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

190 # Cast to float to handle fractional pixels 

191 box = geom.Box2D(box) 

192 inputs["dataIds"] = [ 

193 butlerQC.registry.expandDataId(el.dataId) for el in inputRefs.sourceCatalogs 

194 ] 

195 inputs["wcs"] = wcs 

196 inputs["box"] = box 

197 inputs["doApplyExternalSkyWcs"] = self.config.doApplyExternalSkyWcs 

198 inputs["doApplyExternalPhotoCalib"] = self.config.doApplyExternalPhotoCalib 

199 

200 if self.config.doApplyExternalPhotoCalib: 

201 if self.config.useGlobalExternalPhotoCalib: 

202 externalPhotoCalibCatalog = inputs.pop( 

203 "externalPhotoCalibGlobalCatalog" 

204 ) 

205 else: 

206 externalPhotoCalibCatalog = inputs.pop("externalPhotoCalibTractCatalog") 

207 

208 flatPhotoCalibList = np.hstack(externalPhotoCalibCatalog) 

209 visitPhotoCalibList = np.array( 

210 [calib["visit"] for calib in flatPhotoCalibList] 

211 ) 

212 detectorPhotoCalibList = np.array( 

213 [calib["id"] for calib in flatPhotoCalibList] 

214 ) 

215 

216 if self.config.doApplyExternalSkyWcs: 

217 if self.config.useGlobalExternalSkyWcs: 

218 externalSkyWcsCatalog = inputs.pop("externalSkyWcsGlobalCatalog") 

219 else: 

220 externalSkyWcsCatalog = inputs.pop("externalSkyWcsTractCatalog") 

221 

222 flatSkyWcsList = np.hstack(externalSkyWcsCatalog) 

223 visitSkyWcsList = np.array([calib["visit"] for calib in flatSkyWcsList]) 

224 detectorSkyWcsList = np.array([calib["id"] for calib in flatSkyWcsList]) 

225 

226 if self.config.doApplyExternalPhotoCalib: 

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

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

229 detector = dataId["detector"] 

230 visit = dataId["visit"] 

231 calib_find = (visitPhotoCalibList == visit) & ( 

232 detectorPhotoCalibList == detector 

233 ) 

234 row = flatPhotoCalibList[calib_find] 

235 externalPhotoCalib = row[0].getPhotoCalib() 

236 inputs["photoCalibs"][i] = externalPhotoCalib 

237 

238 if self.config.doApplyExternalSkyWcs: 

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

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

241 detector = dataId["detector"] 

242 visit = dataId["visit"] 

243 calib_find = (visitSkyWcsList == visit) & ( 

244 detectorSkyWcsList == detector 

245 ) 

246 row = flatSkyWcsList[calib_find] 

247 externalSkyWcs = row[0].getWcs() 

248 inputs["astromCalibs"][i] = externalSkyWcs 

249 

250 outputs = self.run(**inputs) 

251 butlerQC.put(outputs, outputRefs) 

252 

253 

254class MatchedTractBaseTask(MatchedBaseTask): 

255 

256 ConfigClass = MatchedBaseConfig 

257 _DefaultName = "matchedTractBaseTask" 

258 

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

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

261 self.radius = self.config.match_radius 

262 self.level = "tract" 

263 

264 def get_box_wcs(self, skymap, oid): 

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

266 wcs = tract_info.getWcs() 

267 tract_box = tract_info.getBBox() 

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

269 return tract_box, wcs