Coverage for python/lsst/meas/base/applyApCorr.py: 28%

133 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-24 02:06 -0700

1# This file is part of meas_base. 

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 

22import math 

23import numpy as np 

24 

25import lsst.afw.image 

26import lsst.pex.config 

27import lsst.pex.exceptions 

28import lsst.pipe.base 

29from lsst.utils.logging import PeriodicLogger 

30from .apCorrRegistry import getApCorrNameSet 

31 

32# If True then scale instFlux error by apCorr; if False then use a more complex computation 

33# that over-estimates instFlux error (often grossly so) because it double-counts photon noise. 

34# This flag is intended to be temporary until we figure out a better way to compute 

35# the effects of aperture correction on instFlux uncertainty 

36UseNaiveFluxErr = True 

37 

38__all__ = ("ApplyApCorrConfig", "ApplyApCorrTask") 

39 

40 

41class ApCorrInfo: 

42 """Catalog field names and keys needed to aperture correct a particular 

43 instrument flux. 

44 

45 Parameters 

46 ---------- 

47 schema : `lsst.afw.table` 

48 Source catalog schema. Three fields are used to generate keys: 

49 - ``{name}_instFlux`` 

50 - ``{name}_instFluxErr`` 

51 - ``{name}_flag`` 

52 Three fields are added: 

53 - ``{name}_apCorr`` (only if not already added by proxy) 

54 - ``{name}_apCorrErr`` (only if not already added by proxy) 

55 - ``{name}_flag_apCorr`` 

56 model : `str` 

57 Field name prefix for instFlux with aperture correction model, e.g. 

58 "base_PsfFlux" 

59 name : `str` 

60 Field name prefix for instFlux needing aperture correction; may be 

61 `None` if it is the same as ``model`` 

62 

63 Notes 

64 ----- 

65 The aperture correction can be derived from the meaasurements in the 

66 column being aperture-corrected or from measurements in a different 

67 column (a "proxy"). In the first case, we will add columns to contain 

68 the aperture correction values; in the second case (using a proxy), 

69 we will add an alias to the proxy's aperture correction values. In 

70 all cases, we add a flag. 

71 """ 

72 

73 name = None 

74 """Field name prefix for flux needing aperture correction (`str`). 

75 """ 

76 

77 modelName = None 

78 """Field name for aperture correction model for flux (`str`). 

79 """ 

80 

81 modelSigmaName = None 

82 """Field name for aperture correction model for fluxErr (`str`). 

83 """ 

84 

85 doApCorrColumn = None 

86 """Should we write the aperture correction values (`bool`)? 

87 

88 They should not be written if they're already being written by a proxy. 

89 """ 

90 

91 instFluxName = None 

92 """Name of ``instFlux`` field (`str`). 

93 """ 

94 

95 instFluxErrName = None 

96 """Name of ``instFlux`` sigma field (`str`). 

97 """ 

98 

99 instFluxKey = None 

100 """Key to ``instFlux`` field (`lsst.afw.table.schema.Key`). 

101 """ 

102 

103 instFluxErrKey = None 

104 """Key to ``instFlux`` sigma field (`lsst.afw.table.schema.Key`). 

105 """ 

106 

107 fluxFlagKey = None 

108 """Key to the flux flag field (`lsst.afw.table.schema.Key`). 

109 """ 

110 

111 apCorrKey = None 

112 """Key to new aperture correction field (`lsst.afw.table.schema.Key`). 

113 """ 

114 

115 apCorrErrKey = None 

116 """Key to new aperture correction sigma field (`lsst.afw.table.schema.Key`). 

117 """ 

118 

119 apCorrFlagKey = None 

120 """Key to new aperture correction flag field (`lsst.afw.table.schema.Key`). 

121 """ 

122 

123 def __init__(self, schema, model, name=None): 

124 if name is None: 

125 name = model 

126 self.name = name 

127 self.modelName = model + "_instFlux" 

128 self.modelSigmaName = model + "_instFluxErr" 

129 self.instFluxName = name + "_instFlux" 

130 self.instFluxErrName = name + "_instFluxErr" 

131 self.instFluxKey = schema.find(self.instFluxName).key 

132 self.instFluxErrKey = schema.find(self.instFluxErrName).key 

133 self.fluxFlagKey = schema.find(name + "_flag").key 

134 

135 # No need to write the same aperture corrections multiple times 

136 self.doApCorrColumn = (name == model or model + "_apCorr" not in schema) 

137 if self.doApCorrColumn: 

138 self.apCorrKey = schema.addField( 

139 name + "_apCorr", 

140 doc="aperture correction applied to %s" % (name,), 

141 type=np.float64, 

142 ) 

143 self.apCorrErrKey = schema.addField( 

144 name + "_apCorrErr", 

145 doc="standard deviation of aperture correction applied to %s" % (name,), 

146 type=np.float64, 

147 ) 

148 else: 

149 aliases = schema.getAliasMap() 

150 aliases.set(name + "_apCorr", model + "_apCorr") 

151 aliases.set(name + "_apCorrErr", model + "_apCorrErr") 

152 self.apCorrKey = schema.find(name + "_apCorr").key 

153 self.apCorrErrKey = schema.find(name + "_apCorrErr").key 

154 

155 self.apCorrFlagKey = schema.addField( 

156 name + "_flag_apCorr", 

157 doc="set if unable to aperture correct %s" % (name,), 

158 type="Flag", 

159 ) 

160 

161 

162class ApplyApCorrConfig(lsst.pex.config.Config): 

163 """Aperture correction configuration. 

164 """ 

165 

166 ignoreList = lsst.pex.config.ListField( 

167 doc="flux measurement algorithms in getApCorrNameSet() to ignore; " 

168 "if a name is listed that does not appear in getApCorrNameSet() then a warning is logged", 

169 dtype=str, 

170 optional=False, 

171 default=(), 

172 ) 

173 doFlagApCorrFailures = lsst.pex.config.Field( 

174 doc="set the general failure flag for a flux when it cannot be aperture-corrected?", 

175 dtype=bool, 

176 default=True, 

177 ) 

178 proxies = lsst.pex.config.DictField( 

179 doc="flux measurement algorithms to be aperture-corrected by reference to another algorithm; " 

180 "this is a mapping alg1:alg2, where 'alg1' is the algorithm being corrected, and 'alg2' " 

181 "is the algorithm supplying the corrections", 

182 keytype=str, 

183 itemtype=str, 

184 default={}, 

185 ) 

186 loggingInterval = lsst.pex.config.Field( 

187 doc="Interval (in seconds) to log messages (at VERBOSE level) while aperture correction is running", 

188 dtype=int, 

189 default=600, 

190 deprecated="This field is no longer used and will be removed in v25", 

191 ) 

192 

193 

194class ApplyApCorrTask(lsst.pipe.base.Task): 

195 """Apply aperture corrections. 

196 

197 Parameters 

198 ---------- 

199 schema : `lsst.afw.table.Schema` 

200 """ 

201 ConfigClass = ApplyApCorrConfig 

202 _DefaultName = "applyApCorr" 

203 

204 def __init__(self, schema, **kwds): 

205 lsst.pipe.base.Task.__init__(self, **kwds) 

206 

207 self.apCorrInfoDict = dict() 

208 apCorrNameSet = getApCorrNameSet() 

209 ignoreSet = set(self.config.ignoreList) 

210 missingNameSet = ignoreSet - set(apCorrNameSet) 

211 if missingNameSet: 

212 self.log.warning("Fields in ignoreList that are not in fluxCorrectList: %s", 

213 sorted(missingNameSet)) 

214 for name in sorted(apCorrNameSet - ignoreSet): 

215 if name + "_instFlux" not in schema: 

216 # if a field in the registry is missing from the schema, silently ignore it. 

217 continue 

218 self.apCorrInfoDict[name] = ApCorrInfo(schema=schema, model=name) 

219 

220 for name, model in self.config.proxies.items(): 

221 if name in apCorrNameSet: 

222 # Already done or ignored 

223 continue 

224 if name + "_instFlux" not in schema: 

225 # Silently ignore 

226 continue 

227 self.apCorrInfoDict[name] = ApCorrInfo(schema=schema, model=model, name=name) 

228 

229 def run(self, catalog, apCorrMap): 

230 """Apply aperture corrections to a catalog of sources. 

231 

232 Parameters 

233 ---------- 

234 catalog : `lsst.afw.table.SourceCatalog` 

235 Catalog of sources. Will be updated in place. 

236 apCorrMap : `lsst.afw.image.ApCorrMap` 

237 Aperture correction map 

238 

239 Notes 

240 ----- 

241 If you show debug-level log messages then you will see statistics for 

242 the effects of aperture correction. 

243 """ 

244 self.log.info("Applying aperture corrections to %d instFlux fields", len(self.apCorrInfoDict)) 

245 if UseNaiveFluxErr: 

246 self.log.debug("Use naive instFlux sigma computation") 

247 else: 

248 self.log.debug("Use complex instFlux sigma computation that double-counts photon noise " 

249 "and thus over-estimates instFlux uncertainty") 

250 

251 # Wrap the task logger to a periodic logger. 

252 periodicLog = PeriodicLogger(self.log) 

253 

254 for apCorrInfo in self.apCorrInfoDict.values(): 

255 apCorrModel = apCorrMap.get(apCorrInfo.modelName) 

256 apCorrErrModel = apCorrMap.get(apCorrInfo.modelSigmaName) 

257 if None in (apCorrModel, apCorrErrModel): 

258 missingNames = [(apCorrInfo.modelName, apCorrInfo.modelSigmaName)[i] 

259 for i, model in enumerate((apCorrModel, apCorrErrModel)) if model is None] 

260 self.log.warning("Cannot aperture correct %s because could not find %s in apCorrMap", 

261 apCorrInfo.name, " or ".join(missingNames)) 

262 for source in catalog: 

263 source.set(apCorrInfo.apCorrFlagKey, True) 

264 continue 

265 

266 for sourceIndex, source in enumerate(catalog): 

267 center = source.getCentroid() 

268 # say we've failed when we start; we'll unset these flags when we succeed 

269 source.set(apCorrInfo.apCorrFlagKey, True) 

270 oldFluxFlagState = False 

271 if self.config.doFlagApCorrFailures: 

272 oldFluxFlagState = source.get(apCorrInfo.fluxFlagKey) 

273 source.set(apCorrInfo.fluxFlagKey, True) 

274 

275 apCorr = 1.0 

276 apCorrErr = 0.0 

277 try: 

278 apCorr = apCorrModel.evaluate(center) 

279 if not UseNaiveFluxErr: 

280 apCorrErr = apCorrErrModel.evaluate(center) 

281 except lsst.pex.exceptions.DomainError: 

282 continue 

283 

284 if apCorrInfo.doApCorrColumn: 

285 source.set(apCorrInfo.apCorrKey, apCorr) 

286 source.set(apCorrInfo.apCorrErrKey, apCorrErr) 

287 

288 if apCorr <= 0.0 or apCorrErr < 0.0: 

289 continue 

290 

291 instFlux = source.get(apCorrInfo.instFluxKey) 

292 instFluxErr = source.get(apCorrInfo.instFluxErrKey) 

293 source.set(apCorrInfo.instFluxKey, instFlux*apCorr) 

294 if UseNaiveFluxErr: 

295 source.set(apCorrInfo.instFluxErrKey, instFluxErr*apCorr) 

296 else: 

297 a = instFluxErr/instFlux 

298 b = apCorrErr/apCorr 

299 source.set(apCorrInfo.instFluxErrKey, abs(instFlux*apCorr)*math.sqrt(a*a + b*b)) 

300 source.set(apCorrInfo.apCorrFlagKey, False) 

301 if self.config.doFlagApCorrFailures: 

302 source.set(apCorrInfo.fluxFlagKey, oldFluxFlagState) 

303 

304 # Log a message if it has been a while since the last log. 

305 periodicLog.log("Aperture corrections applied to %d sources out of %d", 

306 sourceIndex + 1, len(catalog)) 

307 

308 if self.log.isEnabledFor(self.log.DEBUG): 

309 # log statistics on the effects of aperture correction 

310 apCorrArr = np.array([s.get(apCorrInfo.apCorrKey) for s in catalog]) 

311 apCorrErrArr = np.array([s.get(apCorrInfo.apCorrErrKey) for s in catalog]) 

312 self.log.debug("For instFlux field %r: mean apCorr=%s, stdDev apCorr=%s, " 

313 "mean apCorrErr=%s, stdDev apCorrErr=%s for %s sources", 

314 apCorrInfo.name, apCorrArr.mean(), apCorrArr.std(), 

315 apCorrErrArr.mean(), apCorrErrArr.std(), len(catalog))