Coverage for python / lsst / meas / algorithms / normalizedCalibrationFlux.py: 16%

130 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:55 +0000

1# This file is part of lsst.meas.algorithms. 

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__ = ["NormalizedCalibrationFluxConfig", "NormalizedCalibrationFluxTask", 

23 "NormalizedCalibrationFluxError"] 

24 

25import numpy as np 

26 

27from lsst.afw.image import ApCorrMap 

28from lsst.afw.math import ChebyshevBoundedField 

29import lsst.pex.config 

30import lsst.pipe.base 

31from .measureApCorr import MeasureApCorrTask, MeasureApCorrError 

32from .sourceSelector import sourceSelectorRegistry 

33 

34 

35class NormalizedCalibrationFluxError(lsst.pipe.base.AlgorithmError): 

36 """Raised if Aperture Correction fails in a non-recoverable way. 

37 

38 Parameters 

39 ---------- 

40 n_initial_sources : `int` 

41 Number of sources selected by the fallback source selector. 

42 n_calib_flux_flag : `int` 

43 Number of selected sources with raw calibration flux flag unset. 

44 n_ref_flux_flag : `int` 

45 Number of selected sources with reference flux flag unset. 

46 """ 

47 def __init__(self, *, n_initial_sources, n_calib_flux_flag, n_ref_flux_flag): 

48 msg = "There are no valid stars to compute normalized calibration fluxes." 

49 msg += (f" Of {n_initial_sources} initially selected sources, {n_calib_flux_flag} have good raw" 

50 f" calibration fluxes and {n_ref_flux_flag} have good reference fluxes.") 

51 super().__init__(msg) 

52 self.n_initial_sources = n_initial_sources 

53 self.n_calib_flux_flag = n_calib_flux_flag 

54 self.n_ref_flux_flag = n_ref_flux_flag 

55 

56 @property 

57 def metadata(self): 

58 metadata = {"n_init_sources": self.n_initial_sources, 

59 "n_calib_flux_flag": self.n_calib_flux_flag, 

60 "n_ref_flux_flag": self.n_ref_flux_flag} 

61 return metadata 

62 

63 

64class NormalizedCalibrationFluxConfig(lsst.pex.config.Config): 

65 """Configuration parameters for NormalizedCalibrationFluxTask. 

66 """ 

67 measure_ap_corr = lsst.pex.config.ConfigurableField( 

68 target=MeasureApCorrTask, 

69 doc="Subtask to measure aperture corrections.", 

70 ) 

71 raw_calibflux_name = lsst.pex.config.Field( 

72 doc="Name of raw calibration flux to normalize.", 

73 dtype=str, 

74 default="base_CompensatedTophatFlux_12", 

75 ) 

76 normalized_calibflux_name = lsst.pex.config.Field( 

77 doc="Name of normalized calibration flux.", 

78 dtype=str, 

79 default="base_NormalizedCompensatedTophatFlux", 

80 ) 

81 do_set_calib_slot = lsst.pex.config.Field( 

82 doc="Set the calib flux slot to the normalized flux?", 

83 dtype=bool, 

84 default=True, 

85 ) 

86 do_measure_ap_corr = lsst.pex.config.Field( 

87 doc="Measure the aperture correction? (Otherwise, just apply.)", 

88 dtype=bool, 

89 default=True, 

90 ) 

91 fallback_source_selector = sourceSelectorRegistry.makeField( 

92 doc="Selector that is used as a fallback if the full aperture correction " 

93 "fails.", 

94 default="science", 

95 ) 

96 

97 def setDefaults(self): 

98 super().setDefaults() 

99 

100 self.measure_ap_corr.refFluxName = "base_CircularApertureFlux_12_0" 

101 

102 # This task is meant to be used early when we focus on PSF stars. 

103 selector = self.measure_ap_corr.sourceSelector["science"] 

104 selector.doUnresolved = False 

105 selector.flags.good = ["calib_psf_used"] 

106 selector.flags.bad = [] 

107 selector.signalToNoise.fluxField = self.raw_calibflux_name + "_instFlux" 

108 selector.signalToNoise.errField = self.raw_calibflux_name + "_instFluxErr" 

109 # Do median for this. 

110 self.measure_ap_corr.fitConfig.orderX = 0 

111 self.measure_ap_corr.fitConfig.orderY = 0 

112 

113 fallback_selector = self.fallback_source_selector["science"] 

114 fallback_selector.doFluxLimit = False 

115 fallback_selector.doFlags = True 

116 fallback_selector.doUnresolved = False 

117 fallback_selector.doSignalToNoise = False 

118 fallback_selector.doIsolated = False 

119 fallback_selector.flags.good = ["calib_psf_used"] 

120 fallback_selector.flags.bad = [] 

121 

122 

123class NormalizedCalibrationFluxTask(lsst.pipe.base.Task): 

124 """Task to measure the normalized calibration flux. 

125 

126 Parameters 

127 ---------- 

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

129 Schema for the input table; will be modified in place. 

130 **kwargs : `dict` 

131 Additional kwargs to pass to lsst.pipe.base.Task.__init__() 

132 

133 Raises 

134 ------ 

135 NormalizedCalibrationFluxError 

136 Raised if there are not enough sources to calculate normalization. 

137 """ 

138 ConfigClass = NormalizedCalibrationFluxConfig 

139 _DefaultName = "normalizedCalibrationFlux" 

140 

141 def __init__(self, schema, **kwargs): 

142 lsst.pipe.base.Task.__init__(self, **kwargs) 

143 

144 if self.config.do_measure_ap_corr: 

145 self.makeSubtask( 

146 "measure_ap_corr", 

147 schema=schema, 

148 namesToCorrect=[self.config.raw_calibflux_name], 

149 ) 

150 

151 name = self.config.normalized_calibflux_name 

152 self.flux_name = name + "_instFlux" 

153 if self.flux_name not in schema: 

154 schema.addField( 

155 self.flux_name, 

156 type=float, 

157 doc=f"Normalized calibration flux from {self.config.raw_calibflux_name}.", 

158 ) 

159 self.err_name = name + "_instFluxErr" 

160 if self.err_name not in schema: 

161 schema.addField( 

162 self.err_name, 

163 type=float, 

164 doc=f"Normalized calibration flux error from {self.config.raw_calibflux_name}.", 

165 ) 

166 self.flag_name = name + "_flag" 

167 if self.flag_name not in schema: 

168 schema.addField( 

169 self.flag_name, 

170 type="Flag", 

171 doc=f"Normalized calibration flux failure flag from {self.config.raw_calibflux_name}.", 

172 ) 

173 

174 if self.config.do_set_calib_slot: 

175 schema.getAliasMap().set("slot_CalibFlux", name) 

176 

177 self.makeSubtask("fallback_source_selector") 

178 

179 self.schema = schema 

180 

181 def run(self, *, exposure, catalog): 

182 """Measure the Normalized calibration flux. 

183 

184 Parameters 

185 ---------- 

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

187 Exposure the normalized calibration flux is measured on. 

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

189 SourceCatalog containing measurements to be used to compute 

190 normalized calibration fluxes. The catalog is modified in-place. 

191 

192 Returns 

193 ------- 

194 Struct : `lsst.pipe.base.Struct` 

195 Contains the following: 

196 

197 ``ap_corr_map`` 

198 aperture correction map (`lsst.afw.image.ApCorrMap`) 

199 that contains two entries for the raw flux field: 

200 - flux field (e.g. config.{raw_calibflux_name}_instFlux): 2d model 

201 - flux sigma field (e.g. config.{raw_calibflux_name}_instFluxErr): 0 field 

202 """ 

203 self.log.info("Measuring normalized calibration flux from %s", self.config.raw_calibflux_name) 

204 

205 raw_name = self.config.raw_calibflux_name 

206 raw_flux_name = raw_name + "_instFlux" 

207 raw_fluxerr_name = raw_name + "_instFluxErr" 

208 norm_name = self.config.normalized_calibflux_name 

209 

210 if self.config.do_measure_ap_corr: 

211 ap_corr_field, ap_corr_err_field = self._measure_aperture_correction(exposure, catalog) 

212 else: 

213 use_identity = False 

214 ap_corr_map = exposure.info.getApCorrMap() 

215 if ap_corr_map is None: 

216 self.log.warning( 

217 "Exposure does not have a valid normalization map; using identity normalization.", 

218 ) 

219 use_identity = True 

220 else: 

221 ap_corr_field = ap_corr_map.get(raw_flux_name) 

222 ap_corr_err_field = ap_corr_map.get(raw_fluxerr_name) 

223 if not ap_corr_field or not ap_corr_err_field: 

224 self.log.warning( 

225 "Exposure aperture correction map is missing %s/%s for normalization; " 

226 "using identity normalization.", 

227 raw_flux_name, 

228 raw_fluxerr_name, 

229 ) 

230 use_identity = True 

231 

232 if use_identity: 

233 ap_corr_field = ChebyshevBoundedField(exposure.getBBox(), np.array([[1.0]])) 

234 ap_corr_err_field = ChebyshevBoundedField(exposure.getBBox(), np.array([[0.0]])) 

235 

236 corrections = ap_corr_field.evaluate( 

237 catalog["slot_Centroid_x"], 

238 catalog["slot_Centroid_y"], 

239 ) 

240 

241 input_flux_name = raw_flux_name 

242 input_fluxerr_name = raw_fluxerr_name 

243 input_flag_name = raw_name + "_flag" 

244 output_flux_name = norm_name + "_instFlux" 

245 output_fluxerr_name = norm_name + "_instFluxErr" 

246 output_flag_name = norm_name + "_flag" 

247 

248 if catalog.isContiguous(): 

249 catalog[output_flux_name] = catalog[input_flux_name] * corrections 

250 catalog[output_fluxerr_name] = catalog[input_fluxerr_name] * corrections 

251 

252 output_flag = catalog[input_flag_name].copy() 

253 output_flag[corrections <= 0.0] = True 

254 catalog[output_flag_name] = output_flag 

255 else: 

256 # If the catalog is not contiguous we must go row-by-row. 

257 for i, row in enumerate(catalog): 

258 row[output_flux_name] = row[input_flux_name] * corrections[i] 

259 row[output_fluxerr_name] = row[input_fluxerr_name] * corrections[i] 

260 

261 if row[input_flag_name] or corrections[i] <= 0.0: 

262 row[output_flag_name] = True 

263 

264 ap_corr_map = ApCorrMap() 

265 ap_corr_map[raw_flux_name] = ap_corr_field 

266 ap_corr_map[raw_fluxerr_name] = ap_corr_err_field 

267 

268 return lsst.pipe.base.Struct( 

269 ap_corr_map=ap_corr_map, 

270 ) 

271 

272 def _measure_aperture_correction(self, exposure, catalog): 

273 """Internal method to do the aperture correction measurement. 

274 

275 This measures the aperture correction with the regular task, 

276 and if that fails does a fallback median estimate. 

277 

278 Parameters 

279 ---------- 

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

281 Exposure the normalized calibration flux is measured on. 

282 This is only used for the bounding box. 

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

284 SourceCatalog containing measurements to be used to compute 

285 normalized calibration flux. 

286 

287 Returns 

288 ------- 

289 ap_corr_field : `lsst.afw.math.ChebyshevBoundedField` 

290 Aperture correction field to normalize the calibration flux. 

291 ap_corr_err_field : `lsst.afw.math.ChebyshevBoundedField` 

292 Aperture correction to adjust the calibration flux error. 

293 """ 

294 raw_name = self.config.raw_calibflux_name 

295 

296 try: 

297 ap_corr_map = self.measure_ap_corr.run( 

298 exposure=exposure, 

299 catalog=catalog, 

300 ).apCorrMap 

301 

302 ap_corr_field = ap_corr_map.get(raw_name + "_instFlux") 

303 except MeasureApCorrError as e: 

304 self.log.warning("Failed to measure full aperture correction for %s with the following error %s", 

305 raw_name, e) 

306 

307 initSel = self.fallback_source_selector.run(catalog, exposure=exposure).selected 

308 sel = (initSel & ~catalog[self.config.raw_calibflux_name + "_flag"] 

309 & ~catalog[self.config.measure_ap_corr.refFluxName + "_flag"]) 

310 

311 if (n_sel := sel.sum()) == 0: 

312 # This is a fatal error. 

313 raise NormalizedCalibrationFluxError( 

314 n_initial_sources=initSel.sum(), 

315 n_calib_flux_flag=(initSel & ~catalog[self.config.raw_calibflux_name + "_flag"]).sum(), 

316 n_ref_flux_flag=(initSel 

317 & ~catalog[self.config.measure_ap_corr.refFluxName + "_flag"]).sum() 

318 ) 

319 self.log.info("Measuring normalized flux correction with %d stars from fallback selector.", 

320 n_sel) 

321 

322 ratio = np.median( 

323 catalog[self.config.measure_ap_corr.refFluxName + "_instFlux"][sel] 

324 / catalog[self.config.raw_calibflux_name + "_instFlux"][sel] 

325 ) 

326 

327 ap_corr_field = ChebyshevBoundedField( 

328 exposure.getBBox(), 

329 np.array([[ratio]]), 

330 ) 

331 

332 if catalog.isContiguous(): 

333 catalog["apcorr_" + raw_name + "_used"] = sel 

334 else: 

335 for i, row in enumerate(catalog): 

336 row["apcorr_" + raw_name + "_used"] = sel[i] 

337 

338 # We are always setting the error field to 0, because we do not 

339 # have a good model for aperture correction uncertainties. 

340 ap_corr_err_field = ChebyshevBoundedField( 

341 exposure.getBBox(), 

342 np.array([[0.0]]), 

343 ) 

344 

345 return ap_corr_field, ap_corr_err_field