Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 26%

111 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-25 11:39 +0000

1# This file is part of analysis_tools. 

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/>. 

21from __future__ import annotations 

22 

23__all__ = ( 

24 "MatchedRefCoaddToolBase", 

25 "MatchedRefCoaddDiffMetric", 

26 "MatchedRefCoaddDiffMagTool", 

27 "MatchedRefCoaddDiffMagMetric", 

28 "MatchedRefCoaddDiffPositionTool", 

29 "MatchedRefCoaddDiffPositionMetric", 

30) 

31 

32from lsst.pex.config import ChoiceField, Field 

33 

34from ..actions.vector import ( 

35 CalcBinnedStatsAction, 

36 ConstantValue, 

37 DivideVector, 

38 DownselectVector, 

39 LoadVector, 

40 MultiplyVector, 

41 SubtractVector, 

42) 

43from ..actions.vector.selectors import RangeSelector, VectorSelector 

44from .genericBuild import ExtendednessTool, MagnitudeXTool 

45from .genericProduce import MagnitudeScatterPlot 

46 

47 

48class MatchedRefCoaddToolBase(MagnitudeXTool, ExtendednessTool): 

49 """Base tool for matched-to-reference metrics/plots on coadds. 

50 

51 Metrics/plots are expected to use the reference magnitude and 

52 require separate star/galaxy/all source selections. 

53 

54 Notes 

55 ----- 

56 The tool does not use a standard coadd flag selector, because 

57 it is expected that the matcher has been configured to select 

58 appropriate candidates (and stores a match_candidate column). 

59 """ 

60 

61 def setDefaults(self): 

62 super().setDefaults() 

63 self.mag_x = "ref_matched" 

64 

65 def finalize(self): 

66 super().finalize() 

67 self._set_flux_default("mag_x") 

68 

69 

70class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase): 

71 """Base tool for generic diffs between reference and measured values.""" 

72 

73 compute_chi = Field[bool]( 

74 default=False, 

75 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences", 

76 ) 

77 

78 def setDefaults(self): 

79 super().setDefaults() 

80 self.process.filterActions.yAll = DownselectVector( 

81 vectorKey="diff", selector=VectorSelector(vectorKey="allSelector") 

82 ) 

83 self.process.filterActions.yGalaxies = DownselectVector( 

84 vectorKey="diff", selector=VectorSelector(vectorKey="galaxySelector") 

85 ) 

86 self.process.filterActions.yStars = DownselectVector( 

87 vectorKey="diff", selector=VectorSelector(vectorKey="starSelector") 

88 ) 

89 

90 

91class MatchedRefCoaddDiffMetric(MatchedRefCoaddDiffTool): 

92 """Base tool for matched-to-reference metrics on coadds.""" 

93 

94 # These are optional because validate can be called before finalize 

95 # Validate should not fail in that case if it would otherwise succeed 

96 name_prefix = Field[str](doc="Prefix for metric key", default=None, optional=True) 

97 unit = Field[str](doc="Astropy unit of y-axis values", default=None, optional=True) 

98 

99 _mag_low_min: int = 15 

100 _mag_low_max: int = 27 

101 _mag_interval: int = 1 

102 

103 _names = ("stars", "galaxies", "all") 

104 _types = ("unresolved", "resolved", "all") 

105 

106 def configureMetrics( 

107 self, 

108 unit: str | None = None, 

109 name_prefix: str | None = None, 

110 name_suffix: str = "_ref_mag{minimum}", 

111 unit_select: str = "mag", 

112 ): 

113 """Configure metric actions and return units. 

114 

115 Parameters 

116 ---------- 

117 unit : `str` 

118 The (astropy) unit of the summary statistic metrics. 

119 name_prefix : `str` 

120 The prefix for the action (column) name. 

121 name_suffix : `str` 

122 The sufffix for the action (column) name. 

123 unit_select : `str` 

124 The (astropy) unit of the selection (x-axis) column. Default "mag". 

125 

126 Returns 

127 ------- 

128 units : `dict` [`str`, `str`] 

129 A dict of the unit (value) for each metric name (key) 

130 """ 

131 if unit is None: 

132 unit = self.unit if self.unit is not None else "" 

133 if name_prefix is None: 

134 name_prefix = self.name_prefix if self.name_prefix is not None else "" 

135 

136 if unit_select is None: 

137 unit_select = "mag" 

138 

139 key_flux = self.config_mag_x.key_flux 

140 

141 units = {} 

142 for name, name_class in zip(self._names, self._types): 

143 name_capital = name.capitalize() 

144 x_key = f"x{name_capital}" 

145 

146 for minimum in range(self._mag_low_min, self._mag_low_max + 1): 

147 action = getattr(self.process.calculateActions, f"{name}{minimum}") 

148 action.selector_range = RangeSelector( 

149 vectorKey=x_key, 

150 minimum=minimum, 

151 maximum=minimum + self._mag_interval, 

152 ) 

153 

154 action.name_prefix = name_prefix.format(key_flux=key_flux, name_class=name_class) 

155 if self.parameterizedBand: 

156 action.name_prefix = f"{{band}}_{action.name_prefix}" 

157 action.name_suffix = name_suffix.format(minimum=minimum) 

158 

159 units.update( 

160 { 

161 action.name_median: unit, 

162 action.name_sigmaMad: unit, 

163 action.name_count: "count", 

164 action.name_select_minimum: unit_select, 

165 action.name_select_median: unit_select, 

166 action.name_select_maximum: unit_select, 

167 } 

168 ) 

169 return units 

170 

171 def setDefaults(self): 

172 super().setDefaults() 

173 

174 for name in self._names: 

175 key = f"y{name.capitalize()}" 

176 for minimum in range(self._mag_low_min, self._mag_low_max + 1): 

177 setattr( 

178 self.process.calculateActions, 

179 f"{name}{minimum}", 

180 CalcBinnedStatsAction( 

181 key_vector=key, 

182 selector_range=RangeSelector( 

183 vectorKey=key, 

184 minimum=minimum, 

185 maximum=minimum + self._mag_interval, 

186 ), 

187 ), 

188 ) 

189 

190 

191class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool): 

192 """Tool for diffs between reference and measured coadd mags.""" 

193 

194 mag_y = Field[str](default="cmodel_err", doc="Flux (magnitude) field to difference against ref") 

195 

196 def finalize(self): 

197 # Check if it has already been finalized 

198 if not hasattr(self.process.buildActions, "diff"): 

199 # TODO: Is this hack to ensure mag_y is set before plot tools 

200 # are called necessary? 

201 self._set_flux_default("mag_y") 

202 super().finalize() 

203 if self.compute_chi: 

204 self.process.buildActions.diff = DivideVector( 

205 actionA=SubtractVector( 

206 actionA=getattr(self.process.buildActions, f"flux_{self.mag_y}"), 

207 actionB=self.process.buildActions.flux_ref_matched, 

208 ), 

209 actionB=getattr(self.process.buildActions, f"flux_err_{self.mag_y}"), 

210 ) 

211 else: 

212 self.process.buildActions.diff = DivideVector( 

213 actionA=SubtractVector( 

214 actionA=getattr(self.process.buildActions, f"mag_{self.mag_y}"), 

215 actionB=self.process.buildActions.mag_ref_matched, 

216 ), 

217 actionB=ConstantValue(value=1e-3), 

218 ) 

219 

220 

221# The diamond inheritance on MatchedRefCoaddTool seems ok 

222class MatchedRefCoaddDiffMagMetric(MatchedRefCoaddDiffMagTool, MatchedRefCoaddDiffMetric): 

223 """Metric for diffs between reference and measured coadd mags.""" 

224 

225 def finalize(self): 

226 super().finalize() 

227 if self.unit is None: 

228 self.unit = "" if self.compute_chi else "mmag" 

229 if self.name_prefix is None: 

230 subtype = "chi" if self.compute_chi else "diff" 

231 self.name_prefix = f"photom_mag_{{key_flux}}_{{name_class}}_{subtype}_" 

232 if not self.produce.metric.units: 

233 self.produce.metric.units = self.configureMetrics() 

234 

235 

236class MatchedRefCoaddDiffMagPlot(MatchedRefCoaddDiffMagTool, MagnitudeScatterPlot): 

237 def finalize(self): 

238 # TODO: Check if this is really necessary 

239 # finalizing in this order should get all fluxes finalized before 

240 # the MagnitudeScatterPlot looks for a flux to compute S/N from 

241 MatchedRefCoaddDiffMagTool.finalize(self) 

242 MagnitudeScatterPlot.finalize(self) 

243 if not self.produce.yAxisLabel: 

244 config = self.fluxes[self.mag_y] 

245 label = f"{config.name_flux} - {self.fluxes['ref_matched'].name_flux}" 

246 self.produce.yAxisLabel = f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)" 

247 

248 

249class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool): 

250 """Tool for diffs between reference and measured coadd astrometry.""" 

251 

252 scale_factor = Field[float]( 

253 doc="The factor to multiply positions by (i.e. the pixel scale if coordinates have pixel units)", 

254 default=200, 

255 ) 

256 variable = ChoiceField[str]( 

257 doc="The astrometric variable to compute metrics for", 

258 allowed={ 

259 "x": "x", 

260 "y": "y", 

261 }, 

262 optional=False, 

263 ) 

264 

265 def finalize(self): 

266 # Check if it has already been finalized 

267 if not hasattr(self.process.buildActions, "diff"): 

268 super().finalize() 

269 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.variable) 

270 self.process.buildActions.pos_ref = LoadVector(vectorKey=f"refcat_{self.variable}") 

271 if self.compute_chi: 

272 self.process.buildActions.diff = DivideVector( 

273 actionA=SubtractVector( 

274 actionA=self.process.buildActions.pos_meas, 

275 actionB=self.process.buildActions.pos_ref, 

276 ), 

277 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"), 

278 ) 

279 else: 

280 self.process.buildActions.diff = MultiplyVector( 

281 actionA=ConstantValue(value=self.scale_factor), 

282 actionB=SubtractVector( 

283 actionA=self.process.buildActions.pos_meas, 

284 actionB=self.process.buildActions.pos_ref, 

285 ), 

286 ) 

287 

288 

289class MatchedRefCoaddDiffPositionMetric(MatchedRefCoaddDiffPositionTool, MatchedRefCoaddDiffMetric): 

290 """Metric for diffs between reference and base coadd centroids.""" 

291 

292 def finalize(self): 

293 super().finalize() 

294 if self.unit is None: 

295 self.unit = "" if self.compute_chi else "mas" 

296 if self.name_prefix is None: 

297 subtype = "chi" if self.compute_chi else "diff" 

298 self.name_prefix = f"astrom_{self.variable}_{{name_class}}_{subtype}_" 

299 if not self.produce.metric.units: 

300 self.produce.metric.units = self.configureMetrics() 

301 

302 

303class MatchedRefCoaddDiffPositionPlot(MatchedRefCoaddDiffPositionTool, MagnitudeScatterPlot): 

304 # The matched catalog columns are configurable but default to cmodel only 

305 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot") 

306 

307 def finalize(self): 

308 if not self.produce.yAxisLabel: 

309 # Set before MagnitudeScatterPlot.finalize or it'll default to PSF 

310 # Matched ref tables may not have PSF fluxes, or prefer CModel 

311 self._set_flux_default("mag_sn") 

312 super().finalize() 

313 self.produce.yAxisLabel = ( 

314 f"chi = (slot - Reference {self.variable} position)/error" 

315 if self.compute_chi 

316 else f"{self.variable} position (pix)" 

317 )