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

71 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-11 03:00 -0700

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

23 

24import astropy.time 

25 

26import lsst.geom 

27from lsst.daf.base import DateTime 

28import lsst.afw.math as afwMath 

29import lsst.pex.config as pexConfig 

30import lsst.pipe.base as pipeBase 

31from lsst.meas.algorithms import ReferenceSourceSelectorTask 

32from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry 

33from lsst.utils.timer import timeMethod 

34from .matchPessimisticB import MatchPessimisticBTask 

35from .display import displayAstrometry 

36from . import makeMatchStatistics 

37 

38 

39class RefMatchConfig(pexConfig.Config): 

40 matcher = pexConfig.ConfigurableField( 

41 target=MatchPessimisticBTask, 

42 doc="reference object/source matcher", 

43 ) 

44 matchDistanceSigma = pexConfig.RangeField( 

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

46 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; " 

47 "ignored if not fitting a WCS", 

48 dtype=float, 

49 default=2, 

50 min=0, 

51 ) 

52 sourceSelector = sourceSelectorRegistry.makeField( 

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

54 default="science", 

55 ) 

56 referenceSelector = pexConfig.ConfigurableField( 

57 target=ReferenceSourceSelectorTask, 

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

59 ) 

60 sourceFluxType = pexConfig.Field( 

61 dtype=str, 

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

63 default='Calib' 

64 ) 

65 

66 def setDefaults(self): 

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

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

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

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

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

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

73 

74 

75class RefMatchTask(pipeBase.Task): 

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

77 

78 Parameters 

79 ---------- 

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

81 A reference object loader object; gen3 pipeline tasks will pass `None` 

82 and call `setRefObjLoader` in `runQuantum`. 

83 **kwargs 

84 Additional keyword arguments for pipe_base `lsst.pipe.base.Task`. 

85 """ 

86 ConfigClass = RefMatchConfig 

87 _DefaultName = "calibrationBaseClass" 

88 

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

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

91 if refObjLoader: 

92 self.refObjLoader = refObjLoader 

93 else: 

94 self.refObjLoader = None 

95 

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

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

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

99 "the configured sourceFluxType") 

100 

101 self.makeSubtask("matcher") 

102 self.makeSubtask("sourceSelector") 

103 self.makeSubtask("referenceSelector") 

104 

105 def setRefObjLoader(self, refObjLoader): 

106 """Sets the reference object loader for the task. 

107 

108 Parameters 

109 ---------- 

110 refObjLoader 

111 An instance of a reference object loader task or class. 

112 """ 

113 self.refObjLoader = refObjLoader 

114 

115 @timeMethod 

116 def loadAndMatch(self, exposure, sourceCat): 

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

118 detected on that exposure. 

119 

120 Parameters 

121 ---------- 

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

123 exposure that the sources overlap 

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

125 catalog of sources detected on the exposure 

126 

127 Returns 

128 ------- 

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

130 Result struct with Components: 

131 

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

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

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

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

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

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

138 

139 Notes 

140 ----- 

141 ignores config.matchDistanceSigma 

142 """ 

143 if self.refObjLoader is None: 

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

145 import lsstDebug 

146 debug = lsstDebug.Info(__name__) 

147 

148 expMd = self._getExposureMetadata(exposure) 

149 

150 sourceSelection = self.sourceSelector.run(sourceCat) 

151 

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

153 

154 loadRes = self.refObjLoader.loadPixelBox( 

155 bbox=expMd.bbox, 

156 wcs=expMd.wcs, 

157 filterName=expMd.filterName, 

158 epoch=expMd.epoch, 

159 ) 

160 

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

162 

163 matchMeta = self.refObjLoader.getMetadataBox( 

164 bbox=expMd.bbox, 

165 wcs=expMd.wcs, 

166 filterName=expMd.filterName, 

167 epoch=expMd.epoch, 

168 ) 

169 

170 matchRes = self.matcher.matchObjectsToSources( 

171 refCat=refSelection.sourceCat, 

172 sourceCat=sourceSelection.sourceCat, 

173 wcs=expMd.wcs, 

174 sourceFluxField=sourceFluxField, 

175 refFluxField=loadRes.fluxField, 

176 match_tolerance=None, 

177 ) 

178 

179 distStats = self._computeMatchStatsOnSky(matchRes.matches) 

180 self.log.info( 

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

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

183 ) 

184 

185 if debug.display: 

186 frame = int(debug.frame) 

187 displayAstrometry( 

188 refCat=refSelection.sourceCat, 

189 sourceCat=sourceSelection.sourceCat, 

190 matches=matchRes.matches, 

191 exposure=exposure, 

192 bbox=expMd.bbox, 

193 frame=frame, 

194 title="Matches", 

195 ) 

196 

197 return pipeBase.Struct( 

198 refCat=loadRes.refCat, 

199 refSelection=refSelection, 

200 sourceSelection=sourceSelection, 

201 matches=matchRes.matches, 

202 matchMeta=matchMeta, 

203 ) 

204 

205 def _computeMatchStatsOnSky(self, matchList): 

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

207 

208 Parameters 

209 ---------- 

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

211 list of matches between reference object and sources; 

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

213 

214 Returns 

215 ------- 

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

217 Result struct with components: 

218 

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

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

221 separation (`float`) 

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

223 distStdDev (`float`) 

224 """ 

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

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

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

228 return pipeBase.Struct( 

229 distMean=distMean, 

230 distStdDev=distStdDev, 

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

232 ) 

233 

234 def _getExposureMetadata(self, exposure): 

235 """Extract metadata from an exposure. 

236 

237 Parameters 

238 ---------- 

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

240 

241 Returns 

242 ------- 

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

244 Result struct with components: 

245 

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

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

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

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

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

251 """ 

252 filterLabel = exposure.info.getFilter() 

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

254 epoch = None 

255 if exposure.info.hasVisitInfo(): 

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

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

258 

259 return pipeBase.Struct( 

260 bbox=exposure.getBBox(), 

261 wcs=exposure.info.getWcs(), 

262 photoCalib=exposure.info.getPhotoCalib(), 

263 filterName=filterName, 

264 epoch=epoch, 

265 )