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

64 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 09:11 +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='Psf' 

61 ) 

62 

63 def setDefaults(self): 

64 # Configured to match the deprecated "matcher" selector: 

65 # SN > 40, some bad flags, valid centroids. 

66 self.sourceSelector["science"].doSignalToNoise = True 

67 self.sourceSelector["science"].signalToNoise.minimum = 40 

68 self.sourceSelector["science"].signalToNoise.fluxField = f"slot_{self.sourceFluxType}Flux_instFlux" 

69 self.sourceSelector["science"].signalToNoise.errField = f"slot_{self.sourceFluxType}Flux_instFluxErr" 

70 self.sourceSelector["science"].doFlags = True 

71 self.sourceSelector["science"].flags.bad = ["base_PixelFlags_flag_edge", 

72 "base_PixelFlags_flag_nodata", 

73 "base_PixelFlags_flag_interpolatedCenter", 

74 "base_PixelFlags_flag_saturated", 

75 "base_SdssCentroid_flag", 

76 ] 

77 

78 

79class RefMatchTask(pipeBase.Task): 

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

81 

82 Parameters 

83 ---------- 

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

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

86 and call `setRefObjLoader` in `runQuantum`. 

87 **kwargs 

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

89 """ 

90 ConfigClass = RefMatchConfig 

91 _DefaultName = "calibrationBaseClass" 

92 

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

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

95 if refObjLoader: 

96 self.refObjLoader = refObjLoader 

97 else: 

98 self.refObjLoader = None 

99 

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

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

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

103 "the configured sourceFluxType") 

104 

105 self.makeSubtask("matcher") 

106 self.makeSubtask("sourceSelector") 

107 self.makeSubtask("referenceSelector") 

108 

109 def setRefObjLoader(self, refObjLoader): 

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

111 

112 Parameters 

113 ---------- 

114 refObjLoader 

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

116 """ 

117 self.refObjLoader = refObjLoader 

118 

119 @timeMethod 

120 def loadAndMatch(self, exposure, sourceCat): 

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

122 detected on that exposure. 

123 

124 Parameters 

125 ---------- 

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

127 exposure that the sources overlap 

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

129 catalog of sources detected on the exposure 

130 

131 Returns 

132 ------- 

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

134 Result struct with Components: 

135 

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

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

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

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

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

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

142 

143 Notes 

144 ----- 

145 ignores config.matchDistanceSigma 

146 """ 

147 if self.refObjLoader is None: 

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

149 import lsstDebug 

150 debug = lsstDebug.Info(__name__) 

151 

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

153 

154 sourceSelection = self.sourceSelector.run(sourceCat) 

155 

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

157 

158 loadRes = self.refObjLoader.loadPixelBox( 

159 bbox=exposure.getBBox(), 

160 wcs=exposure.wcs, 

161 filterName=exposure.filter.bandLabel, 

162 epoch=epoch, 

163 ) 

164 

165 refSelection = self.referenceSelector.run(loadRes.refCat, exposure=exposure) 

166 

167 matchMeta = self.refObjLoader.getMetadataBox( 

168 bbox=exposure.getBBox(), 

169 wcs=exposure.wcs, 

170 filterName=exposure.filter.bandLabel, 

171 epoch=epoch, 

172 ) 

173 

174 matchRes = self.matcher.matchObjectsToSources( 

175 refCat=refSelection.sourceCat, 

176 sourceCat=sourceSelection.sourceCat, 

177 wcs=exposure.wcs, 

178 sourceFluxField=sourceFluxField, 

179 refFluxField=loadRes.fluxField, 

180 matchTolerance=None, 

181 bbox=exposure.getBBox(), 

182 ) 

183 

184 distStats = self._computeMatchStatsOnSky(matchRes.matches) 

185 self.log.info( 

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

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

188 ) 

189 

190 if debug.display: 

191 frame = int(debug.frame) 

192 displayAstrometry( 

193 refCat=refSelection.sourceCat, 

194 sourceCat=sourceSelection.sourceCat, 

195 matches=matchRes.matches, 

196 exposure=exposure, 

197 bbox=exposure.getBBox(), 

198 frame=frame, 

199 title="Matches", 

200 ) 

201 

202 return pipeBase.Struct( 

203 refCat=loadRes.refCat, 

204 refSelection=refSelection, 

205 sourceSelection=sourceSelection, 

206 matches=matchRes.matches, 

207 matchMeta=matchMeta, 

208 ) 

209 

210 def _computeMatchStatsOnSky(self, matchList): 

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

212 

213 Parameters 

214 ---------- 

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

216 list of matches between reference object and sources; 

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

218 

219 Returns 

220 ------- 

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

222 Result struct with components: 

223 

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

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

226 separation (`float`) 

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

228 distStdDev (`float`) 

229 """ 

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

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

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

233 return pipeBase.Struct( 

234 distMean=distMean, 

235 distStdDev=distStdDev, 

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

237 )