Coverage for python/lsst/meas/astrom/ref_match.py: 34%

Shortcuts 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

73 statements  

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__ = ['RefMatchConfig', 'RefMatchTask'] 

24 

25import astropy.time 

26 

27import lsst.geom 

28from lsst.daf.base import DateTime 

29import lsst.afw.math as afwMath 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32from lsst.meas.algorithms import ReferenceSourceSelectorTask 

33from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry 

34from lsst.utils.timer import timeMethod 

35from .matchPessimisticB import MatchPessimisticBTask 

36from .display import displayAstrometry 

37from . import makeMatchStatistics 

38 

39 

40class RefMatchConfig(pexConfig.Config): 

41 matcher = pexConfig.ConfigurableField( 

42 target=MatchPessimisticBTask, 

43 doc="reference object/source matcher", 

44 ) 

45 matchDistanceSigma = pexConfig.RangeField( 

46 doc="the maximum match distance is set to " 

47 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; " 

48 "ignored if not fitting a WCS", 

49 dtype=float, 

50 default=2, 

51 min=0, 

52 ) 

53 sourceSelector = sourceSelectorRegistry.makeField( 

54 doc="How to select sources for cross-matching.", 

55 default="science", 

56 ) 

57 referenceSelector = pexConfig.ConfigurableField( 

58 target=ReferenceSourceSelectorTask, 

59 doc="How to select reference objects for cross-matching." 

60 ) 

61 sourceFluxType = pexConfig.Field( 

62 dtype=str, 

63 doc="Source flux type to use in source selection.", 

64 default='Calib' 

65 ) 

66 

67 def setDefaults(self): 

68 self.sourceSelector.name = "science" 

69 self.sourceSelector['science'].fluxLimit.fluxField = \ 

70 'slot_%sFlux_instFlux' % (self.sourceFluxType) 

71 self.sourceSelector['science'].signalToNoise.fluxField = \ 

72 'slot_%sFlux_instFlux' % (self.sourceFluxType) 

73 self.sourceSelector['science'].signalToNoise.errField = \ 

74 'slot_%sFlux_instFluxErr' % (self.sourceFluxType) 

75 

76 

77class RefMatchTask(pipeBase.Task): 

78 """Match an input source catalog with objects from a reference catalog. 

79 

80 Parameters 

81 ---------- 

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

83 A reference object loader object 

84 **kwargs 

85 additional keyword arguments for pipe_base `lsst.pipe.base.Task` 

86 """ 

87 ConfigClass = RefMatchConfig 

88 _DefaultName = "calibrationBaseClass" 

89 

90 def __init__(self, refObjLoader, **kwargs): 

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

92 if refObjLoader: 

93 self.refObjLoader = refObjLoader 

94 else: 

95 self.refObjLoader = None 

96 

97 if self.config.sourceSelector.name == 'matcher': 

98 if self.config.sourceSelector['matcher'].sourceFluxType != self.config.sourceFluxType: 

99 raise RuntimeError("The sourceFluxType in the sourceSelector['matcher'] must match " 

100 "the configured sourceFluxType") 

101 

102 self.makeSubtask("matcher") 

103 self.makeSubtask("sourceSelector") 

104 self.makeSubtask("referenceSelector") 

105 

106 def setRefObjLoader(self, refObjLoader): 

107 """Sets the reference object loader for the task 

108 

109 Parameters 

110 ---------- 

111 refObjLoader 

112 An instance of a reference object loader task or class 

113 """ 

114 self.refObjLoader = refObjLoader 

115 

116 @timeMethod 

117 def loadAndMatch(self, exposure, sourceCat): 

118 """Load reference objects overlapping an exposure and match to sources 

119 detected on that exposure. 

120 

121 Parameters 

122 ---------- 

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

124 exposure that the sources overlap 

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

126 catalog of sources detected on the exposure 

127 

128 Returns 

129 ------- 

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

131 Result struct with Components: 

132 

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

134 exposure (`lsst.afw.table.SimpleCatalog`) 

135 - ``matches`` : Matched sources and references 

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

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

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

139 

140 Notes 

141 ----- 

142 ignores config.matchDistanceSigma 

143 """ 

144 if self.refObjLoader is None: 

145 raise RuntimeError("Running matcher task with no refObjLoader set in __ini__ or setRefObjLoader") 

146 import lsstDebug 

147 debug = lsstDebug.Info(__name__) 

148 

149 expMd = self._getExposureMetadata(exposure) 

150 

151 sourceSelection = self.sourceSelector.run(sourceCat) 

152 

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

154 

155 loadRes = self.refObjLoader.loadPixelBox( 

156 bbox=expMd.bbox, 

157 wcs=expMd.wcs, 

158 filterName=expMd.filterName, 

159 photoCalib=expMd.photoCalib, 

160 epoch=expMd.epoch, 

161 ) 

162 

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

164 

165 matchMeta = self.refObjLoader.getMetadataBox( 

166 bbox=expMd.bbox, 

167 wcs=expMd.wcs, 

168 filterName=expMd.filterName, 

169 photoCalib=expMd.photoCalib, 

170 epoch=expMd.epoch, 

171 ) 

172 

173 matchRes = self.matcher.matchObjectsToSources( 

174 refCat=refSelection.sourceCat, 

175 sourceCat=sourceSelection.sourceCat, 

176 wcs=expMd.wcs, 

177 sourceFluxField=sourceFluxField, 

178 refFluxField=loadRes.fluxField, 

179 match_tolerance=None, 

180 ) 

181 

182 distStats = self._computeMatchStatsOnSky(matchRes.matches) 

183 self.log.info( 

184 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; ", 

185 len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds() 

186 ) 

187 

188 if debug.display: 

189 frame = int(debug.frame) 

190 displayAstrometry( 

191 refCat=refSelection.sourceCat, 

192 sourceCat=sourceSelection.sourceCat, 

193 matches=matchRes.matches, 

194 exposure=exposure, 

195 bbox=expMd.bbox, 

196 frame=frame, 

197 title="Matches", 

198 ) 

199 

200 return pipeBase.Struct( 

201 refCat=loadRes.refCat, 

202 refSelection=refSelection, 

203 sourceSelection=sourceSelection, 

204 matches=matchRes.matches, 

205 matchMeta=matchMeta, 

206 ) 

207 

208 def _computeMatchStatsOnSky(self, matchList): 

209 """Compute on-sky radial distance statistics for a match list 

210 

211 Parameters 

212 ---------- 

213 matchList : `list` of `lsst.afw.table.ReferenceMatch` 

214 list of matches between reference object and sources; 

215 the distance field is the only field read and it must be set to distance in radians 

216 

217 Returns 

218 ------- 

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

220 Result struct with components: 

221 

222 - ``distMean`` : clipped mean of on-sky radial separation (`float`) 

223 - ``distStdDev`` : clipped standard deviation of on-sky radial 

224 separation (`float`) 

225 - ``maxMatchDist`` : distMean + self.config.matchDistanceSigma * 

226 distStdDev (`float`) 

227 """ 

228 distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP) 

229 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*lsst.geom.radians 

230 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*lsst.geom.radians 

231 return pipeBase.Struct( 

232 distMean=distMean, 

233 distStdDev=distStdDev, 

234 maxMatchDist=distMean + self.config.matchDistanceSigma * distStdDev, 

235 ) 

236 

237 def _getExposureMetadata(self, exposure): 

238 """Extract metadata from an exposure. 

239 

240 Parameters 

241 ---------- 

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

243 

244 Returns 

245 ------- 

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

247 Result struct with components: 

248 

249 - ``bbox`` : parent bounding box (`lsst.geom.Box2I`) 

250 - ``wcs`` : exposure WCS (`lsst.afw.geom.SkyWcs`) 

251 - ``photoCalib`` : photometric calibration (`lsst.afw.image.PhotoCalib`) 

252 - ``filterName`` : name of filter band (`str`) 

253 - ``epoch`` : date of exposure (`astropy.time.Time`) 

254 

255 """ 

256 exposureInfo = exposure.getInfo() 

257 filterLabel = exposureInfo.getFilterLabel() 

258 filterName = filterLabel.bandLabel if filterLabel is not None else None 

259 epoch = None 

260 if exposure.getInfo().hasVisitInfo(): 

261 epochTaiMjd = exposure.getInfo().getVisitInfo().getDate().get(system=DateTime.MJD, 

262 scale=DateTime.TAI) 

263 epoch = astropy.time.Time(epochTaiMjd, scale="tai", format="mjd") 

264 

265 return pipeBase.Struct( 

266 bbox=exposure.getBBox(), 

267 wcs=exposureInfo.getWcs(), 

268 photoCalib=exposureInfo.getPhotoCalib(), 

269 filterName=filterName, 

270 epoch=epoch, 

271 )