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

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.pipe.base as pipeBase 

23import lsst.pex.config as pexConfig 

24import lsst.geom as geom 

25import numpy as np 

26 

27from lsst.faro.utils.matcher import matchCatalogs 

28 

29__all__ = ( 

30 "MatchedBaseConnections", 

31 "MatchedBaseConfig", 

32 "MatchedBaseTask", 

33 "MatchedTractBaseTask", 

34) 

35 

36 

37class MatchedBaseConnections( 

38 pipeBase.PipelineTaskConnections, 

39 dimensions=(), 

40 defaultTemplates={ 

41 "coaddName": "deep", 

42 "photoCalibName": "calexp.photoCalib", 

43 "wcsName": "calexp.wcs", 

44 "externalPhotoCalibName": "fgcm", 

45 "externalWcsName": "jointcal", 

46 }, 

47): 

48 sourceCatalogs = pipeBase.connectionTypes.Input( 

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

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

51 storageClass="SourceCatalog", 

52 name="src", 

53 multiple=True, 

54 ) 

55 photoCalibs = pipeBase.connectionTypes.Input( 

56 doc="Photometric calibration object.", 

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

58 storageClass="PhotoCalib", 

59 name="{photoCalibName}", 

60 multiple=True, 

61 ) 

62 astromCalibs = pipeBase.connectionTypes.Input( 

63 doc="WCS for the catalog.", 

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

65 storageClass="Wcs", 

66 name="{wcsName}", 

67 multiple=True, 

68 ) 

69 externalSkyWcsTractCatalog = pipeBase.connectionTypes.Input( 

70 doc=( 

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

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

73 ), 

74 name="{externalWcsName}SkyWcsCatalog", 

75 storageClass="ExposureCatalog", 

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

77 multiple=True, 

78 ) 

79 externalSkyWcsGlobalCatalog = pipeBase.connectionTypes.Input( 

80 doc=( 

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

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

83 "fast lookup." 

84 ), 

85 name="{externalWcsName}SkyWcsCatalog", 

86 storageClass="ExposureCatalog", 

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

88 multiple=True, 

89 ) 

90 externalPhotoCalibTractCatalog = pipeBase.connectionTypes.Input( 

91 doc=( 

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

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

94 ), 

95 name="{externalPhotoCalibName}PhotoCalibCatalog", 

96 storageClass="ExposureCatalog", 

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

98 multiple=True, 

99 ) 

100 externalPhotoCalibGlobalCatalog = pipeBase.connectionTypes.Input( 

101 doc=( 

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

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

104 "sorted on id for fast lookup." 

105 ), 

106 name="{externalPhotoCalibName}PhotoCalibCatalog", 

107 storageClass="ExposureCatalog", 

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

109 multiple=True, 

110 ) 

111 skyMap = pipeBase.connectionTypes.Input( 

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

113 name="skyMap", 

114 storageClass="SkyMap", 

115 dimensions=("skymap",), 

116 ) 

117 

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

119 super().__init__(config=config) 

120 if config.doApplyExternalSkyWcs: 

121 if config.useGlobalExternalSkyWcs: 

122 self.inputs.remove("externalSkyWcsTractCatalog") 

123 else: 

124 self.inputs.remove("externalSkyWcsGlobalCatalog") 

125 else: 

126 self.inputs.remove("externalSkyWcsTractCatalog") 

127 self.inputs.remove("externalSkyWcsGlobalCatalog") 

128 if config.doApplyExternalPhotoCalib: 

129 if config.useGlobalExternalPhotoCalib: 

130 self.inputs.remove("externalPhotoCalibTractCatalog") 

131 else: 

132 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

133 else: 

134 self.inputs.remove("externalPhotoCalibTractCatalog") 

135 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

136 

137 

138class MatchedBaseConfig( 

139 pipeBase.PipelineTaskConfig, pipelineConnections=MatchedBaseConnections 

140): 

141 match_radius = pexConfig.Field( 

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

143 ) 

144 doApplyExternalSkyWcs = pexConfig.Field( 

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

146 ) 

147 useGlobalExternalSkyWcs = pexConfig.Field( 

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

149 ) 

150 doApplyExternalPhotoCalib = pexConfig.Field( 

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

152 ) 

153 useGlobalExternalPhotoCalib = pexConfig.Field( 

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

155 dtype=bool, 

156 default=False, 

157 ) 

158 

159 

160class MatchedBaseTask(pipeBase.PipelineTask): 

161 

162 ConfigClass = MatchedBaseConfig 

163 _DefaultName = "matchedBaseTask" 

164 

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

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

167 self.radius = self.config.match_radius 

168 self.level = "patch" 

169 

170 def run( 

171 self, 

172 sourceCatalogs, 

173 photoCalibs, 

174 astromCalibs, 

175 dataIds, 

176 wcs, 

177 box, 

178 doApplyExternalSkyWcs=False, 

179 doApplyExternalPhotoCalib=False, 

180 ): 

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

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

183 srcvis, matched = matchCatalogs( 

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

185 ) 

186 # Trim the output to the patch bounding box 

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

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

189 for record in matched: 

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

191 out_matched.append(record) 

192 self.log.info( 

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

194 ) 

195 return pipeBase.Struct(outputCatalog=out_matched) 

196 

197 def get_box_wcs(self, skymap, oid): 

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

199 wcs = tract_info.getWcs() 

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

201 patch_box = patch_info.getInnerBBox() 

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

203 return patch_box, wcs 

204 

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

206 inputs = butlerQC.get(inputRefs) 

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

208 skymap = inputs["skyMap"] 

209 del inputs["skyMap"] 

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

211 # Cast to float to handle fractional pixels 

212 box = geom.Box2D(box) 

213 inputs["dataIds"] = [ 

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

215 ] 

216 inputs["wcs"] = wcs 

217 inputs["box"] = box 

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

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

220 

221 if self.config.doApplyExternalPhotoCalib: 

222 if self.config.useGlobalExternalPhotoCalib: 

223 externalPhotoCalibCatalog = inputs.pop( 

224 "externalPhotoCalibGlobalCatalog" 

225 ) 

226 else: 

227 externalPhotoCalibCatalog = inputs.pop("externalPhotoCalibTractCatalog") 

228 

229 flatPhotoCalibList = np.hstack(externalPhotoCalibCatalog) 

230 visitPhotoCalibList = np.array( 

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

232 ) 

233 detectorPhotoCalibList = np.array( 

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

235 ) 

236 

237 if self.config.doApplyExternalSkyWcs: 

238 if self.config.useGlobalExternalSkyWcs: 

239 externalSkyWcsCatalog = inputs.pop("externalSkyWcsGlobalCatalog") 

240 else: 

241 externalSkyWcsCatalog = inputs.pop("externalSkyWcsTractCatalog") 

242 

243 flatSkyWcsList = np.hstack(externalSkyWcsCatalog) 

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

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

246 

247 if self.config.doApplyExternalPhotoCalib: 

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

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

250 detector = dataId["detector"] 

251 visit = dataId["visit"] 

252 calib_find = (visitPhotoCalibList == visit) & ( 

253 detectorPhotoCalibList == detector 

254 ) 

255 row = flatPhotoCalibList[calib_find] 

256 externalPhotoCalib = row[0].getPhotoCalib() 

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

258 

259 if self.config.doApplyExternalSkyWcs: 

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

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

262 detector = dataId["detector"] 

263 visit = dataId["visit"] 

264 calib_find = (visitSkyWcsList == visit) & ( 

265 detectorSkyWcsList == detector 

266 ) 

267 row = flatSkyWcsList[calib_find] 

268 externalSkyWcs = row[0].getWcs() 

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

270 

271 outputs = self.run(**inputs) 

272 butlerQC.put(outputs, outputRefs) 

273 

274 

275class MatchedTractBaseTask(MatchedBaseTask): 

276 

277 ConfigClass = MatchedBaseConfig 

278 _DefaultName = "matchedTractBaseTask" 

279 

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

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

282 self.radius = self.config.match_radius 

283 self.level = "tract" 

284 

285 def get_box_wcs(self, skymap, oid): 

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

287 wcs = tract_info.getWcs() 

288 tract_box = tract_info.getBBox() 

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

290 return tract_box, wcs