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

72 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-12 09:40 +0000

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 epoch=expMd.epoch, 

160 ) 

161 

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

163 

164 matchMeta = self.refObjLoader.getMetadataBox( 

165 bbox=expMd.bbox, 

166 wcs=expMd.wcs, 

167 filterName=expMd.filterName, 

168 epoch=expMd.epoch, 

169 ) 

170 

171 matchRes = self.matcher.matchObjectsToSources( 

172 refCat=refSelection.sourceCat, 

173 sourceCat=sourceSelection.sourceCat, 

174 wcs=expMd.wcs, 

175 sourceFluxField=sourceFluxField, 

176 refFluxField=loadRes.fluxField, 

177 match_tolerance=None, 

178 ) 

179 

180 distStats = self._computeMatchStatsOnSky(matchRes.matches) 

181 self.log.info( 

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

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

184 ) 

185 

186 if debug.display: 

187 frame = int(debug.frame) 

188 displayAstrometry( 

189 refCat=refSelection.sourceCat, 

190 sourceCat=sourceSelection.sourceCat, 

191 matches=matchRes.matches, 

192 exposure=exposure, 

193 bbox=expMd.bbox, 

194 frame=frame, 

195 title="Matches", 

196 ) 

197 

198 return pipeBase.Struct( 

199 refCat=loadRes.refCat, 

200 refSelection=refSelection, 

201 sourceSelection=sourceSelection, 

202 matches=matchRes.matches, 

203 matchMeta=matchMeta, 

204 ) 

205 

206 def _computeMatchStatsOnSky(self, matchList): 

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

208 

209 Parameters 

210 ---------- 

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

212 list of matches between reference object and sources; 

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

214 

215 Returns 

216 ------- 

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

218 Result struct with components: 

219 

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

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

222 separation (`float`) 

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

224 distStdDev (`float`) 

225 """ 

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

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

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

229 return pipeBase.Struct( 

230 distMean=distMean, 

231 distStdDev=distStdDev, 

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

233 ) 

234 

235 def _getExposureMetadata(self, exposure): 

236 """Extract metadata from an exposure. 

237 

238 Parameters 

239 ---------- 

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

241 

242 Returns 

243 ------- 

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

245 Result struct with components: 

246 

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

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

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

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

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

252 """ 

253 filterLabel = exposure.info.getFilter() 

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

255 epoch = None 

256 if exposure.info.hasVisitInfo(): 

257 epochTaiMjd = exposure.visitInfo.date.get(system=DateTime.MJD, scale=DateTime.TAI) 

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

259 

260 return pipeBase.Struct( 

261 bbox=exposure.getBBox(), 

262 wcs=exposure.info.getWcs(), 

263 photoCalib=exposure.info.getPhotoCalib(), 

264 filterName=filterName, 

265 epoch=epoch, 

266 )