Coverage for python/lsst/meas/astrom/matchOptimisticBTask.py: 30%

89 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-18 12:18 -0700

1 

2__all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig", 

3 "MatchTolerance"] 

4 

5import math 

6 

7import lsst.pex.config as pexConfig 

8import lsst.pipe.base as pipeBase 

9from lsst.utils.timer import timeMethod 

10 

11from .setMatchDistance import setMatchDistance 

12from .matchOptimisticB import matchOptimisticB, MatchOptimisticBControl 

13 

14 

15class MatchTolerance: 

16 """Stores match tolerances for use in `lsst.meas.astrom.AstrometryTask` and 

17 later iterations of the matcher. 

18 

19 MatchOptimsiticBTask relies on a maximum distance for matching 

20 set by either the default in MatchOptimisticBConfig or the 2 sigma 

21 scatter found after AstrometryTask has fit for a wcs. 

22 

23 Parameters 

24 ---------- 

25 maxMatchDist : `lsst.geom.Angle` 

26 Current maximum distance to consider a match. 

27 """ 

28 

29 def __init__(self, maxMatchDist=None): 

30 self.maxMatchDist = maxMatchDist 

31 

32 

33class MatchOptimisticBConfig(pexConfig.Config): 

34 """Configuration for MatchOptimisticBTask 

35 """ 

36 maxMatchDistArcSec = pexConfig.RangeField( 

37 doc="Maximum separation between reference objects and sources " 

38 "beyond which they will not be considered a match (arcsec)", 

39 dtype=float, 

40 default=3, 

41 min=0, 

42 ) 

43 numBrightStars = pexConfig.RangeField( 

44 doc="Number of bright stars to use", 

45 dtype=int, 

46 default=50, 

47 min=2, 

48 ) 

49 minMatchedPairs = pexConfig.RangeField( 

50 doc="Minimum number of matched pairs; see also minFracMatchedPairs", 

51 dtype=int, 

52 default=30, 

53 min=2, 

54 ) 

55 minFracMatchedPairs = pexConfig.RangeField( 

56 doc="Minimum number of matched pairs as a fraction of the smaller of " 

57 "the number of reference stars or the number of good sources; " 

58 "the actual minimum is the smaller of this value or minMatchedPairs", 

59 dtype=float, 

60 default=0.3, 

61 min=0, 

62 max=1, 

63 ) 

64 maxOffsetPix = pexConfig.RangeField( 

65 doc="Maximum allowed shift of WCS, due to matching (pixel). " 

66 "When changing this value, the LoadReferenceObjectsConfig.pixelMargin should also be updated.", 

67 dtype=int, 

68 default=250, 

69 max=4000, 

70 ) 

71 maxRotationDeg = pexConfig.RangeField( 

72 doc="Rotation angle allowed between sources and position reference objects (degrees)", 

73 dtype=float, 

74 default=1.0, 

75 max=6.0, 

76 ) 

77 allowedNonperpDeg = pexConfig.RangeField( 

78 doc="Allowed non-perpendicularity of x and y (degree)", 

79 dtype=float, 

80 default=3.0, 

81 max=45.0, 

82 ) 

83 numPointsForShape = pexConfig.Field( 

84 doc="number of points to define a shape for matching", 

85 dtype=int, 

86 default=6, 

87 ) 

88 maxDeterminant = pexConfig.Field( 

89 doc="maximum determinant of linear transformation matrix for a usable solution", 

90 dtype=float, 

91 default=0.02, 

92 ) 

93 

94 

95# The following block adds links to this task from the Task Documentation page. 

96# \addtogroup LSST_task_documentation 

97# \{ 

98# \page measAstrom_matchOptimisticBTask 

99# \ref MatchOptimisticBTask "MatchOptimisticBTask" 

100# Match sources to reference objects 

101# \} 

102 

103 

104class MatchOptimisticBTask(pipeBase.Task): 

105 """Match sources to reference objects using the Optimistic Pattern Matcher 

106 B algorithm of Tabur 2007. 

107 """ 

108 ConfigClass = MatchOptimisticBConfig 

109 _DefaultName = "matchObjectsToSources" 

110 

111 def __init__(self, **kwargs): 

112 pipeBase.Task.__init__(self, **kwargs) 

113 

114 def filterStars(self, refCat): 

115 """Extra filtering pass; subclass if desired. 

116 

117 Parameters 

118 ---------- 

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

120 Catalog of reference objects. 

121 

122 Returns 

123 ------- 

124 trimmedRefCat : `lsst.afw.table.SimpleCatalog` 

125 Reference catalog with some filtering applied. Currently no 

126 filtering is applied. 

127 """ 

128 return refCat 

129 

130 @timeMethod 

131 def matchObjectsToSources(self, refCat, sourceCat, wcs, sourceFluxField, refFluxField, 

132 match_tolerance=None): 

133 """Match sources to position reference stars. 

134 

135 Parameters 

136 ---------- 

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

138 Reference catalog to match. 

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

140 Catalog of sources found on an exposure. This should already be 

141 down-selected to "good"/"usable" sources in the calling Task. 

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

143 Current WCS of the exposure containing the sources. 

144 sourceFluxField : `str` 

145 Field of the sourceCat to use for flux 

146 refFluxField : `str` 

147 Field of the refCat to use for flux 

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

149 Object containing information from previous 

150 `lsst.meas.astrom.AstrometryTask` match/fit cycles for use in 

151 matching. If `None` is config defaults. 

152 

153 Returns 

154 ------- 

155 matchResult : `lsst.pipe.base.Struct` 

156 Result struct with components 

157 

158 - ``matches`` : List of matches with distance below the maximum match 

159 distance (`list` of `lsst.afw.table.ReferenceMatch`). 

160 - ``useableSourceCat`` : Catalog of sources matched and suited for 

161 WCS fitting (`lsst.afw.table.SourceCatalog`). 

162 - ``match_tolerance`` : MatchTolerance object updated from this 

163 match iteration (`lsst.meas.astrom.MatchTolerance`). 

164 """ 

165 import lsstDebug 

166 debug = lsstDebug.Info(__name__) 

167 

168 preNumObj = len(refCat) 

169 refCat = self.filterStars(refCat) 

170 numRefObj = len(refCat) 

171 

172 if self.log: 

173 self.log.info("filterStars purged %d reference stars, leaving %d stars", 

174 preNumObj - numRefObj, numRefObj) 

175 

176 if match_tolerance is None: 

177 match_tolerance = MatchTolerance() 

178 

179 # Make a name alias here for consistency with older code, and to make 

180 # it clear that this is a good/usable (cleaned) source catalog. 

181 usableSourceCat = sourceCat 

182 

183 numUsableSources = len(usableSourceCat) 

184 

185 if len(usableSourceCat) == 0: 

186 raise pipeBase.TaskError("No sources are usable") 

187 

188 minMatchedPairs = min(self.config.minMatchedPairs, 

189 int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)]))) 

190 

191 # match usable (possibly saturated) sources and then purge saturated sources from the match list 

192 usableMatches = self._doMatch( 

193 refCat=refCat, 

194 sourceCat=usableSourceCat, 

195 wcs=wcs, 

196 refFluxField=refFluxField, 

197 numUsableSources=numUsableSources, 

198 minMatchedPairs=minMatchedPairs, 

199 maxMatchDist=match_tolerance.maxMatchDist, 

200 sourceFluxField=sourceFluxField, 

201 verbose=debug.verbose, 

202 ) 

203 

204 # cull non-good sources 

205 matches = [] 

206 self._getIsGoodKeys(usableSourceCat.schema) 

207 for match in usableMatches: 

208 if self._isGoodTest(match.second): 

209 # Append the isGood match. 

210 matches.append(match) 

211 

212 self.log.debug("Found %d usable matches, of which %d had good sources", 

213 len(usableMatches), len(matches)) 

214 

215 if len(matches) == 0: 

216 raise RuntimeError("Unable to match sources") 

217 

218 self.log.info("Matched %d sources", len(matches)) 

219 if len(matches) < minMatchedPairs: 

220 self.log.warning("Number of matches is smaller than request") 

221 

222 return pipeBase.Struct( 

223 matches=matches, 

224 usableSourceCat=usableSourceCat, 

225 match_tolerance=match_tolerance, 

226 ) 

227 

228 def _getIsGoodKeys(self, schema): 

229 """Retrieve the keys needed for the isGoodTest from the source catalog 

230 schema. 

231 

232 Parameters 

233 ---------- 

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

235 Source schema to retrieve `lsst.afw.table.Key` s from. 

236 """ 

237 self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey() 

238 self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey() 

239 self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey() 

240 

241 def _isGoodTest(self, source): 

242 """Test that an object is good for use in the WCS fitter. 

243 

244 This is a hard coded version of the isGood flag from the old SourceInfo 

245 class that used to be part of this class. 

246 

247 Parameters 

248 ---------- 

249 source : `lsst.afw.table.SourceRecord` 

250 Source to test. 

251 

252 Returns 

253 ------- 

254 isGood : `bool` 

255 Source passes CCD edge and saturated tests. 

256 """ 

257 return (not source.get(self.edgeKey) 

258 and not source.get(self.interpolatedCenterKey) 

259 and not source.get(self.saturatedKey)) 

260 

261 @timeMethod 

262 def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, 

263 maxMatchDist, sourceFluxField, verbose): 

264 """Implementation of matching sources to position reference stars. 

265 

266 Unlike matchObjectsToSources, this method does not check if the sources 

267 are suitable. 

268 

269 Parameters 

270 ---------- 

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

272 Catalog of reference objects. 

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

274 Catalog of detected sources. 

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

276 Current best WCS of the image. 

277 refFluxFioeld : `str` 

278 Name of flux field in refCat to use. 

279 numUsableSources : `int` 

280 Total number of source usable for matching. 

281 mintMatchPairs : `int` 

282 Minimum number of objects to match between the refCat and sourceCat 

283 to consider a valid match. 

284 maxMatchDist : `lsst.geom.Angle` 

285 Maximum separation to considering a reference and a source a match. 

286 sourceFluxField : `str` 

287 Name of source catalog flux field. 

288 verbose : `bool` 

289 Print diagnostic information std::cout 

290 

291 Returns 

292 ------- 

293 matches : `list` of `lsst.afw.table.ReferenceMatch` 

294 """ 

295 numSources = len(sourceCat) 

296 posRefBegInd = numUsableSources - numSources 

297 if maxMatchDist is None: 

298 maxMatchDistArcSec = self.config.maxMatchDistArcSec 

299 else: 

300 maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec) 

301 configMatchDistPix = maxMatchDistArcSec/wcs.getPixelScale().asArcseconds() 

302 

303 matchControl = MatchOptimisticBControl() 

304 matchControl.refFluxField = refFluxField 

305 matchControl.sourceFluxField = sourceFluxField 

306 matchControl.numBrightStars = self.config.numBrightStars 

307 matchControl.minMatchedPairs = self.config.minMatchedPairs 

308 matchControl.maxOffsetPix = self.config.maxOffsetPix 

309 matchControl.numPointsForShape = self.config.numPointsForShape 

310 matchControl.maxDeterminant = self.config.maxDeterminant 

311 

312 for maxRotInd in range(4): 

313 matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd) 

314 for matchRadInd in range(3): 

315 matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd) 

316 

317 for angleDiffInd in range(3): 

318 matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1) 

319 matches = matchOptimisticB( 

320 refCat, 

321 sourceCat, 

322 matchControl, 

323 wcs, 

324 posRefBegInd, 

325 verbose, 

326 ) 

327 if matches is not None and len(matches) > 0: 

328 setMatchDistance(matches) 

329 return matches 

330 return matches