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

61 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-03 10:36 +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__ = ['RefMatchConfig', 'RefMatchTask'] 

23 

24import lsst.geom 

25import lsst.afw.math as afwMath 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28from lsst.meas.algorithms import ReferenceSourceSelectorTask 

29from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry 

30from lsst.utils.timer import timeMethod 

31from .matchPessimisticB import MatchPessimisticBTask 

32from .display import displayAstrometry 

33from . import makeMatchStatistics 

34 

35 

36class RefMatchConfig(pexConfig.Config): 

37 matcher = pexConfig.ConfigurableField( 

38 target=MatchPessimisticBTask, 

39 doc="reference object/source matcher", 

40 ) 

41 matchDistanceSigma = pexConfig.RangeField( 

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

43 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; " 

44 "ignored if not fitting a WCS", 

45 dtype=float, 

46 default=2, 

47 min=0, 

48 ) 

49 sourceSelector = sourceSelectorRegistry.makeField( 

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

51 default="science", 

52 ) 

53 referenceSelector = pexConfig.ConfigurableField( 

54 target=ReferenceSourceSelectorTask, 

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

56 ) 

57 sourceFluxType = pexConfig.Field( 

58 dtype=str, 

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

60 default='Calib' 

61 ) 

62 

63 def setDefaults(self): 

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

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

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

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

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

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

70 

71 

72class RefMatchTask(pipeBase.Task): 

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

74 

75 Parameters 

76 ---------- 

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

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

79 and call `setRefObjLoader` in `runQuantum`. 

80 **kwargs 

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

82 """ 

83 ConfigClass = RefMatchConfig 

84 _DefaultName = "calibrationBaseClass" 

85 

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

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

88 if refObjLoader: 

89 self.refObjLoader = refObjLoader 

90 else: 

91 self.refObjLoader = None 

92 

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

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

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

96 "the configured sourceFluxType") 

97 

98 self.makeSubtask("matcher") 

99 self.makeSubtask("sourceSelector") 

100 self.makeSubtask("referenceSelector") 

101 

102 def setRefObjLoader(self, refObjLoader): 

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

104 

105 Parameters 

106 ---------- 

107 refObjLoader 

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

109 """ 

110 self.refObjLoader = refObjLoader 

111 

112 @timeMethod 

113 def loadAndMatch(self, exposure, sourceCat): 

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

115 detected on that exposure. 

116 

117 Parameters 

118 ---------- 

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

120 exposure that the sources overlap 

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

122 catalog of sources detected on the exposure 

123 

124 Returns 

125 ------- 

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

127 Result struct with Components: 

128 

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

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

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

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

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

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

135 

136 Notes 

137 ----- 

138 ignores config.matchDistanceSigma 

139 """ 

140 if self.refObjLoader is None: 

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

142 import lsstDebug 

143 debug = lsstDebug.Info(__name__) 

144 

145 epoch = exposure.visitInfo.date.toAstropy() 

146 

147 sourceSelection = self.sourceSelector.run(sourceCat) 

148 

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

150 

151 loadRes = self.refObjLoader.loadPixelBox( 

152 bbox=exposure.getBBox(), 

153 wcs=exposure.wcs, 

154 filterName=exposure.filter.bandLabel, 

155 epoch=epoch, 

156 ) 

157 

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

159 

160 matchMeta = self.refObjLoader.getMetadataBox( 

161 bbox=exposure.getBBox(), 

162 wcs=exposure.wcs, 

163 filterName=exposure.filter.bandLabel, 

164 epoch=epoch, 

165 ) 

166 

167 matchRes = self.matcher.matchObjectsToSources( 

168 refCat=refSelection.sourceCat, 

169 sourceCat=sourceSelection.sourceCat, 

170 wcs=exposure.wcs, 

171 sourceFluxField=sourceFluxField, 

172 refFluxField=loadRes.fluxField, 

173 matchTolerance=None, 

174 ) 

175 

176 distStats = self._computeMatchStatsOnSky(matchRes.matches) 

177 self.log.info( 

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

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

180 ) 

181 

182 if debug.display: 

183 frame = int(debug.frame) 

184 displayAstrometry( 

185 refCat=refSelection.sourceCat, 

186 sourceCat=sourceSelection.sourceCat, 

187 matches=matchRes.matches, 

188 exposure=exposure, 

189 bbox=exposure.getBBox(), 

190 frame=frame, 

191 title="Matches", 

192 ) 

193 

194 return pipeBase.Struct( 

195 refCat=loadRes.refCat, 

196 refSelection=refSelection, 

197 sourceSelection=sourceSelection, 

198 matches=matchRes.matches, 

199 matchMeta=matchMeta, 

200 ) 

201 

202 def _computeMatchStatsOnSky(self, matchList): 

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

204 

205 Parameters 

206 ---------- 

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

208 list of matches between reference object and sources; 

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

210 

211 Returns 

212 ------- 

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

214 Result struct with components: 

215 

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

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

218 separation (`float`) 

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

220 distStdDev (`float`) 

221 """ 

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

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

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

225 return pipeBase.Struct( 

226 distMean=distMean, 

227 distStdDev=distStdDev, 

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

229 )