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# 

2# LSST Data Management System 

3# Copyright 2008-2016 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23__all__ = ["AstrometryConfig", "AstrometryTask"] 

24 

25 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28from .ref_match import RefMatchTask, RefMatchConfig 

29from .fitTanSipWcs import FitTanSipWcsTask 

30from .display import displayAstrometry 

31 

32 

33class AstrometryConfig(RefMatchConfig): 

34 """Config for AstrometryTask. 

35 """ 

36 wcsFitter = pexConfig.ConfigurableField( 

37 target=FitTanSipWcsTask, 

38 doc="WCS fitter", 

39 ) 

40 forceKnownWcs = pexConfig.Field( 

41 dtype=bool, 

42 doc="If True then load reference objects and match sources but do not fit a WCS; " 

43 "this simply controls whether 'run' calls 'solve' or 'loadAndMatch'", 

44 default=False, 

45 ) 

46 maxIter = pexConfig.RangeField( 

47 doc="maximum number of iterations of match sources and fit WCS" 

48 "ignored if not fitting a WCS", 

49 dtype=int, 

50 default=3, 

51 min=1, 

52 ) 

53 minMatchDistanceArcSec = pexConfig.RangeField( 

54 doc="the match distance below which further iteration is pointless (arcsec); " 

55 "ignored if not fitting a WCS", 

56 dtype=float, 

57 default=0.001, 

58 min=0, 

59 ) 

60 

61 def setDefaults(self): 

62 # Override the default source selector for astrometry tasks 

63 self.sourceFluxType = "Ap" 

64 

65 self.sourceSelector.name = "matcher" 

66 self.sourceSelector["matcher"].sourceFluxType = self.sourceFluxType 

67 

68 # Note that if the matcher is MatchOptimisticBTask, then the 

69 # default should be self.sourceSelector['matcher'].excludePixelFlags = False 

70 # However, there is no way to do this automatically. 

71 

72 

73class AstrometryTask(RefMatchTask): 

74 """Match an input source catalog with objects from a reference catalog and 

75 solve for the WCS. 

76 

77 This task is broken into two main subasks: matching and WCS fitting which 

78 are very interactive. The matching here can be considered in part a first 

79 pass WCS fitter due to the fitter's sensitivity to outliers. 

80 

81 Parameters 

82 ---------- 

83 refObjLoader : `lsst.meas.algorithms.ReferenceLoader` 

84 A reference object loader object 

85 schema : `lsst.afw.table.Schema` 

86 Used to set "calib_astrometry_used" flag in output source catalog. 

87 **kwargs 

88 additional keyword arguments for pipe_base 

89 `lsst.pipe.base.Task.__init__` 

90 """ 

91 ConfigClass = AstrometryConfig 

92 _DefaultName = "astrometricSolver" 

93 

94 def __init__(self, refObjLoader, schema=None, **kwargs): 

95 RefMatchTask.__init__(self, refObjLoader, **kwargs) 

96 

97 if schema is not None: 

98 self.usedKey = schema.addField("calib_astrometry_used", type="Flag", 

99 doc="set if source was used in astrometric calibration") 

100 else: 

101 self.usedKey = None 

102 

103 self.makeSubtask("wcsFitter") 

104 

105 @pipeBase.timeMethod 

106 def run(self, sourceCat, exposure): 

107 """Load reference objects, match sources and optionally fit a WCS. 

108 

109 This is a thin layer around solve or loadAndMatch, depending on 

110 config.forceKnownWcs. 

111 

112 Parameters 

113 ---------- 

114 exposure : `lsst.afw.image.Exposure` 

115 exposure whose WCS is to be fit 

116 The following are read only: 

117 

118 - bbox 

119 - photoCalib (may be absent) 

120 - filter (may be unset) 

121 - detector (if wcs is pure tangent; may be absent) 

122 

123 The following are updated: 

124 

125 - wcs (the initial value is used as an initial guess, and is 

126 required) 

127 

128 sourceCat : `lsst.afw.table.SourceCatalog` 

129 catalog of sources detected on the exposure 

130 

131 Returns 

132 ------- 

133 result : `lsst.pipe.base.Struct` 

134 with these fields: 

135 

136 - ``refCat`` : reference object catalog of objects that overlap the 

137 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`). 

138 - ``matches`` : astrometric matches 

139 (`list` of `lsst.afw.table.ReferenceMatch`). 

140 - ``scatterOnSky`` : median on-sky separation between reference 

141 objects and sources in "matches" 

142 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True 

143 - ``matchMeta`` : metadata needed to unpersist matches 

144 (`lsst.daf.base.PropertyList`) 

145 """ 

146 if self.refObjLoader is None: 

147 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader") 

148 if self.config.forceKnownWcs: 

149 res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat) 

150 res.scatterOnSky = None 

151 else: 

152 res = self.solve(exposure=exposure, sourceCat=sourceCat) 

153 return res 

154 

155 @pipeBase.timeMethod 

156 def solve(self, exposure, sourceCat): 

157 """Load reference objects overlapping an exposure, match to sources and 

158 fit a WCS 

159 

160 Returns 

161 ------- 

162 result : `lsst.pipe.base.Struct` 

163 Result struct with components: 

164 

165 - ``refCat`` : reference object catalog of objects that overlap the 

166 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`). 

167 - ``matches`` : astrometric matches 

168 (`list` of `lsst.afw.table.ReferenceMatch`). 

169 - ``scatterOnSky`` : median on-sky separation between reference 

170 objects and sources in "matches" (`lsst.geom.Angle`) 

171 - ``matchMeta`` : metadata needed to unpersist matches 

172 (`lsst.daf.base.PropertyList`) 

173 

174 Notes 

175 ----- 

176 ignores config.forceKnownWcs 

177 """ 

178 if self.refObjLoader is None: 

179 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader") 

180 import lsstDebug 

181 debug = lsstDebug.Info(__name__) 

182 

183 expMd = self._getExposureMetadata(exposure) 

184 

185 sourceSelection = self.sourceSelector.run(sourceCat) 

186 

187 self.log.info("Purged %d sources, leaving %d good sources" % 

188 (len(sourceCat) - len(sourceSelection.sourceCat), 

189 len(sourceSelection.sourceCat))) 

190 

191 loadRes = self.refObjLoader.loadPixelBox( 

192 bbox=expMd.bbox, 

193 wcs=expMd.wcs, 

194 filterName=expMd.filterName, 

195 photoCalib=expMd.photoCalib, 

196 epoch=expMd.epoch, 

197 ) 

198 

199 refSelection = self.referenceSelector.run(loadRes.refCat) 

200 

201 matchMeta = self.refObjLoader.getMetadataBox( 

202 bbox=expMd.bbox, 

203 wcs=expMd.wcs, 

204 filterName=expMd.filterName, 

205 photoCalib=expMd.photoCalib, 

206 epoch=expMd.epoch, 

207 ) 

208 

209 if debug.display: 

210 frame = int(debug.frame) 

211 displayAstrometry( 

212 refCat=refSelection.sourceCat, 

213 sourceCat=sourceSelection.sourceCat, 

214 exposure=exposure, 

215 bbox=expMd.bbox, 

216 frame=frame, 

217 title="Reference catalog", 

218 ) 

219 

220 res = None 

221 wcs = expMd.wcs 

222 match_tolerance = None 

223 for i in range(self.config.maxIter): 

224 iterNum = i + 1 

225 try: 

226 tryRes = self._matchAndFitWcs( 

227 refCat=refSelection.sourceCat, 

228 sourceCat=sourceCat, 

229 goodSourceCat=sourceSelection.sourceCat, 

230 refFluxField=loadRes.fluxField, 

231 bbox=expMd.bbox, 

232 wcs=wcs, 

233 exposure=exposure, 

234 match_tolerance=match_tolerance, 

235 ) 

236 except Exception as e: 

237 # if we have had a succeessful iteration then use that; otherwise fail 

238 if i > 0: 

239 self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e)) 

240 iterNum -= 1 

241 break 

242 else: 

243 raise 

244 

245 match_tolerance = tryRes.match_tolerance 

246 tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches) 

247 self.log.debug( 

248 "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; " 

249 "max match distance = %0.3f arcsec", 

250 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(), 

251 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds()) 

252 

253 maxMatchDist = tryMatchDist.maxMatchDist 

254 res = tryRes 

255 wcs = res.wcs 

256 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec: 

257 self.log.debug( 

258 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; " 

259 "that's good enough", 

260 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec) 

261 break 

262 match_tolerance.maxMatchDist = maxMatchDist 

263 

264 self.log.info( 

265 "Matched and fit WCS in %d iterations; " 

266 "found %d matches with scatter = %0.3f +- %0.3f arcsec" % 

267 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(), 

268 tryMatchDist.distStdDev.asArcseconds())) 

269 for m in res.matches: 

270 if self.usedKey: 

271 m.second.set(self.usedKey, True) 

272 exposure.setWcs(res.wcs) 

273 

274 return pipeBase.Struct( 

275 refCat=refSelection.sourceCat, 

276 matches=res.matches, 

277 scatterOnSky=res.scatterOnSky, 

278 matchMeta=matchMeta, 

279 ) 

280 

281 @pipeBase.timeMethod 

282 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance, 

283 exposure=None): 

284 """Match sources to reference objects and fit a WCS. 

285 

286 Parameters 

287 ---------- 

288 refCat : `lsst.afw.table.SimpleCatalog` 

289 catalog of reference objects 

290 sourceCat : `lsst.afw.table.SourceCatalog` 

291 catalog of sources detected on the exposure 

292 goodSourceCat : `lsst.afw.table.SourceCatalog` 

293 catalog of down-selected good sources detected on the exposure 

294 refFluxField : 'str' 

295 field of refCat to use for flux 

296 bbox : `lsst.geom.Box2I` 

297 bounding box of exposure 

298 wcs : `lsst.afw.geom.SkyWcs` 

299 initial guess for WCS of exposure 

300 match_tolerance : `lsst.meas.astrom.MatchTolerance` 

301 a MatchTolerance object (or None) specifying 

302 internal tolerances to the matcher. See the MatchTolerance 

303 definition in the respective matcher for the class definition. 

304 exposure : `lsst.afw.image.Exposure` 

305 exposure whose WCS is to be fit, or None; used only for the debug 

306 display. 

307 

308 Returns 

309 ------- 

310 result : `lsst.pipe.base.Struct` 

311 Result struct with components: 

312 

313 - ``matches``: astrometric matches 

314 (`list` of `lsst.afw.table.ReferenceMatch`). 

315 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs). 

316 - ``scatterOnSky`` : median on-sky separation between reference 

317 objects and sources in "matches" (`lsst.afw.geom.Angle`). 

318 """ 

319 import lsstDebug 

320 debug = lsstDebug.Info(__name__) 

321 

322 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType) 

323 

324 matchRes = self.matcher.matchObjectsToSources( 

325 refCat=refCat, 

326 sourceCat=goodSourceCat, 

327 wcs=wcs, 

328 sourceFluxField=sourceFluxField, 

329 refFluxField=refFluxField, 

330 match_tolerance=match_tolerance, 

331 ) 

332 self.log.debug("Found %s matches", len(matchRes.matches)) 

333 if debug.display: 

334 frame = int(debug.frame) 

335 displayAstrometry( 

336 refCat=refCat, 

337 sourceCat=matchRes.usableSourceCat, 

338 matches=matchRes.matches, 

339 exposure=exposure, 

340 bbox=bbox, 

341 frame=frame + 1, 

342 title="Initial WCS", 

343 ) 

344 

345 self.log.debug("Fitting WCS") 

346 fitRes = self.wcsFitter.fitWcs( 

347 matches=matchRes.matches, 

348 initWcs=wcs, 

349 bbox=bbox, 

350 refCat=refCat, 

351 sourceCat=sourceCat, 

352 exposure=exposure, 

353 ) 

354 fitWcs = fitRes.wcs 

355 scatterOnSky = fitRes.scatterOnSky 

356 if debug.display: 

357 frame = int(debug.frame) 

358 displayAstrometry( 

359 refCat=refCat, 

360 sourceCat=matchRes.usableSourceCat, 

361 matches=matchRes.matches, 

362 exposure=exposure, 

363 bbox=bbox, 

364 frame=frame + 2, 

365 title="Fit TAN-SIP WCS", 

366 ) 

367 

368 return pipeBase.Struct( 

369 matches=matchRes.matches, 

370 wcs=fitWcs, 

371 scatterOnSky=scatterOnSky, 

372 match_tolerance=matchRes.match_tolerance, 

373 )