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

89 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-25 18:23 +0000

1# This file is part of meas_astrom. 

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 

22__all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig", 

23 "MatchTolerance"] 

24 

25import math 

26 

27import lsst.pex.config as pexConfig 

28import lsst.pipe.base as pipeBase 

29from lsst.utils.timer import timeMethod 

30 

31from .setMatchDistance import setMatchDistance 

32from .matchOptimisticB import matchOptimisticB, MatchOptimisticBControl 

33 

34 

35class MatchTolerance: 

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

37 later iterations of the matcher. 

38 

39 MatchOptimsiticBTask relies on a maximum distance for matching 

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

41 scatter found after AstrometryTask has fit for a wcs. 

42 

43 Parameters 

44 ---------- 

45 maxMatchDist : `lsst.geom.Angle` 

46 Current maximum distance to consider a match. 

47 """ 

48 

49 def __init__(self, maxMatchDist=None): 

50 self.maxMatchDist = maxMatchDist 

51 

52 

53class MatchOptimisticBConfig(pexConfig.Config): 

54 """Configuration for MatchOptimisticBTask 

55 """ 

56 maxMatchDistArcSec = pexConfig.RangeField( 

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

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

59 dtype=float, 

60 default=2.0, 

61 min=0, 

62 ) 

63 numBrightStars = pexConfig.RangeField( 

64 doc="Maximum number of bright stars to use in fit.", 

65 dtype=int, 

66 default=150, 

67 min=2, 

68 ) 

69 minMatchedPairs = pexConfig.RangeField( 

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

71 dtype=int, 

72 default=30, 

73 min=2, 

74 ) 

75 minFracMatchedPairs = pexConfig.RangeField( 

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

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

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

79 dtype=float, 

80 default=0.3, 

81 min=0, 

82 max=1, 

83 ) 

84 maxOffsetPix = pexConfig.RangeField( 

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

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

87 dtype=int, 

88 default=250, 

89 max=4000, 

90 ) 

91 maxRotationDeg = pexConfig.RangeField( 

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

93 dtype=float, 

94 default=1.0, 

95 max=6.0, 

96 ) 

97 allowedNonperpDeg = pexConfig.RangeField( 

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

99 dtype=float, 

100 default=0.2, 

101 max=45.0, 

102 ) 

103 numPointsForShape = pexConfig.Field( 

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

105 dtype=int, 

106 default=6, 

107 ) 

108 maxDeterminant = pexConfig.Field( 

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

110 dtype=float, 

111 default=0.02, 

112 ) 

113 

114 

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

116# \addtogroup LSST_task_documentation 

117# \{ 

118# \page measAstrom_matchOptimisticBTask 

119# \ref MatchOptimisticBTask "MatchOptimisticBTask" 

120# Match sources to reference objects 

121# \} 

122 

123 

124class MatchOptimisticBTask(pipeBase.Task): 

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

126 B algorithm of Tabur 2007. 

127 """ 

128 ConfigClass = MatchOptimisticBConfig 

129 _DefaultName = "matchObjectsToSources" 

130 

131 def __init__(self, **kwargs): 

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

133 

134 def filterStars(self, refCat): 

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

136 

137 Parameters 

138 ---------- 

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

140 Catalog of reference objects. 

141 

142 Returns 

143 ------- 

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

145 Reference catalog with some filtering applied. Currently no 

146 filtering is applied. 

147 """ 

148 return refCat 

149 

150 @timeMethod 

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

152 matchTolerance=None): 

153 """Match sources to position reference stars. 

154 

155 Parameters 

156 ---------- 

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

158 Reference catalog to match. 

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

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

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

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

163 Current WCS of the exposure containing the sources. 

164 sourceFluxField : `str` 

165 Field of the sourceCat to use for flux 

166 refFluxField : `str` 

167 Field of the refCat to use for flux 

168 matchTolerance : `lsst.meas.astrom.MatchTolerance` 

169 Object containing information from previous 

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

171 matching. If `None` is config defaults. 

172 

173 Returns 

174 ------- 

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

176 Result struct with components 

177 

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

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

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

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

182 - ``matchTolerance`` : MatchTolerance object updated from this 

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

184 """ 

185 import lsstDebug 

186 debug = lsstDebug.Info(__name__) 

187 

188 preNumObj = len(refCat) 

189 refCat = self.filterStars(refCat) 

190 numRefObj = len(refCat) 

191 

192 if self.log: 

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

194 preNumObj - numRefObj, numRefObj) 

195 

196 if matchTolerance is None: 

197 matchTolerance = MatchTolerance() 

198 

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

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

201 usableSourceCat = sourceCat 

202 

203 numUsableSources = len(usableSourceCat) 

204 

205 if len(usableSourceCat) == 0: 

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

207 

208 minMatchedPairs = min(self.config.minMatchedPairs, 

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

210 

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

212 usableMatches = self._doMatch( 

213 refCat=refCat, 

214 sourceCat=usableSourceCat, 

215 wcs=wcs, 

216 refFluxField=refFluxField, 

217 numUsableSources=numUsableSources, 

218 minMatchedPairs=minMatchedPairs, 

219 maxMatchDist=matchTolerance.maxMatchDist, 

220 sourceFluxField=sourceFluxField, 

221 verbose=debug.verbose, 

222 ) 

223 

224 # cull non-good sources 

225 matches = [] 

226 self._getIsGoodKeys(usableSourceCat.schema) 

227 for match in usableMatches: 

228 if self._isGoodTest(match.second): 

229 # Append the isGood match. 

230 matches.append(match) 

231 

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

233 len(usableMatches), len(matches)) 

234 

235 if len(matches) == 0: 

236 raise RuntimeError("Unable to match sources") 

237 

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

239 if len(matches) < minMatchedPairs: 

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

241 

242 return pipeBase.Struct( 

243 matches=matches, 

244 usableSourceCat=usableSourceCat, 

245 matchTolerance=matchTolerance, 

246 ) 

247 

248 def _getIsGoodKeys(self, schema): 

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

250 schema. 

251 

252 Parameters 

253 ---------- 

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

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

256 """ 

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

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

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

260 

261 def _isGoodTest(self, source): 

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

263 

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

265 class that used to be part of this class. 

266 

267 Parameters 

268 ---------- 

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

270 Source to test. 

271 

272 Returns 

273 ------- 

274 isGood : `bool` 

275 Source passes CCD edge and saturated tests. 

276 """ 

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

278 and not source.get(self.interpolatedCenterKey) 

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

280 

281 @timeMethod 

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

283 maxMatchDist, sourceFluxField, verbose): 

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

285 

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

287 are suitable. 

288 

289 Parameters 

290 ---------- 

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

292 Catalog of reference objects. 

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

294 Catalog of detected sources. 

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

296 Current best WCS of the image. 

297 refFluxFioeld : `str` 

298 Name of flux field in refCat to use. 

299 numUsableSources : `int` 

300 Total number of source usable for matching. 

301 mintMatchPairs : `int` 

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

303 to consider a valid match. 

304 maxMatchDist : `lsst.geom.Angle` 

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

306 sourceFluxField : `str` 

307 Name of source catalog flux field. 

308 verbose : `bool` 

309 Print diagnostic information std::cout 

310 

311 Returns 

312 ------- 

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

314 """ 

315 numSources = len(sourceCat) 

316 posRefBegInd = numUsableSources - numSources 

317 if maxMatchDist is None: 

318 maxMatchDistArcSec = self.config.maxMatchDistArcSec 

319 else: 

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

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

322 

323 matchControl = MatchOptimisticBControl() 

324 matchControl.refFluxField = refFluxField 

325 matchControl.sourceFluxField = sourceFluxField 

326 matchControl.numBrightStars = self.config.numBrightStars 

327 matchControl.minMatchedPairs = self.config.minMatchedPairs 

328 matchControl.maxOffsetPix = self.config.maxOffsetPix 

329 matchControl.numPointsForShape = self.config.numPointsForShape 

330 matchControl.maxDeterminant = self.config.maxDeterminant 

331 

332 for maxRotInd in range(4): 

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

334 for matchRadInd in range(3): 

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

336 

337 for angleDiffInd in range(3): 

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

339 matches = matchOptimisticB( 

340 refCat, 

341 sourceCat, 

342 matchControl, 

343 wcs, 

344 posRefBegInd, 

345 verbose, 

346 ) 

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

348 setMatchDistance(matches) 

349 return matches 

350 return matches