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

98 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-10 14:10 +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 "MatchedRefCoaddDiffTool", 

26 "MatchedRefCoaddDiffMagTool", 

27 "MatchedRefCoaddDiffPositionTool", 

28) 

29 

30from lsst.pex.config import Field 

31 

32from ..actions.vector import ( 

33 CalcBinnedStatsAction, 

34 ConstantValue, 

35 DivideVector, 

36 DownselectVector, 

37 LoadVector, 

38 MultiplyVector, 

39 SubtractVector, 

40) 

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

42from .genericBuild import ExtendednessTool, MagnitudeXTool 

43from .genericProduce import MagnitudeScatterPlot 

44 

45 

46class MatchedRefCoaddToolBase(MagnitudeXTool, ExtendednessTool): 

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

48 

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

50 require separate star/galaxy/all source selections. 

51 

52 Notes 

53 ----- 

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

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

56 appropriate candidates (and stores a match_candidate column). 

57 """ 

58 

59 def setDefaults(self): 

60 super().setDefaults() 

61 self.mag_x = "ref_matched" 

62 

63 def finalize(self): 

64 super().finalize() 

65 self._set_flux_default("mag_x") 

66 

67 

68class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase): 

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

70 

71 compute_chi = Field[bool]( 

72 default=False, 

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

74 ) 

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

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

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

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

79 

80 _mag_low_min: int = 15 

81 _mag_low_max: int = 27 

82 _mag_interval: int = 1 

83 

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

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

86 

87 def configureMetrics( 

88 self, 

89 unit: str | None = None, 

90 name_prefix: str | None = None, 

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

92 unit_select: str = "mag", 

93 ): 

94 """Configure metric actions and return units. 

95 

96 Parameters 

97 ---------- 

98 unit : `str` 

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

100 name_prefix : `str` 

101 The prefix for the action (column) name. 

102 name_suffix : `str` 

103 The sufffix for the action (column) name. 

104 unit_select : `str` 

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

106 

107 Returns 

108 ------- 

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

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

111 """ 

112 if unit is None: 

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

114 if name_prefix is None: 

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

116 

117 if unit_select is None: 

118 unit_select = "mag" 

119 

120 key_flux = self.config_mag_x.key_flux 

121 

122 units = {} 

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

124 name_capital = name.capitalize() 

125 x_key = f"x{name_capital}" 

126 

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

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

129 action.selector_range = RangeSelector( 

130 vectorKey=x_key, 

131 minimum=minimum, 

132 maximum=minimum + self._mag_interval, 

133 ) 

134 

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

136 if self.parameterizedBand: 

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

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

139 

140 units.update( 

141 { 

142 action.name_median: unit, 

143 action.name_sigmaMad: unit, 

144 action.name_count: "count", 

145 action.name_select_minimum: unit_select, 

146 action.name_select_median: unit_select, 

147 action.name_select_maximum: unit_select, 

148 } 

149 ) 

150 return units 

151 

152 def setDefaults(self): 

153 super().setDefaults() 

154 

155 self.process.filterActions.yAll = DownselectVector( 

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

157 ) 

158 self.process.filterActions.yGalaxies = DownselectVector( 

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

160 ) 

161 self.process.filterActions.yStars = DownselectVector( 

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

163 ) 

164 

165 for name in self._names: 

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

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

168 setattr( 

169 self.process.calculateActions, 

170 f"{name}{minimum}", 

171 CalcBinnedStatsAction( 

172 key_vector=key, 

173 selector_range=RangeSelector( 

174 vectorKey=key, 

175 minimum=minimum, 

176 maximum=minimum + self._mag_interval, 

177 ), 

178 ), 

179 ) 

180 

181 

182class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

184 

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

186 

187 def finalize(self): 

188 # Check if it has already been finalized 

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

190 # Ensure mag_y is set before any plot finalizes 

191 self._set_flux_default("mag_y") 

192 super().finalize() 

193 if self.compute_chi: 

194 self.process.buildActions.diff = DivideVector( 

195 actionA=SubtractVector( 

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

197 actionB=self.process.buildActions.flux_ref_matched, 

198 ), 

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

200 ) 

201 else: 

202 self.process.buildActions.diff = DivideVector( 

203 actionA=SubtractVector( 

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

205 actionB=self.process.buildActions.mag_ref_matched, 

206 ), 

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

208 ) 

209 if not self.produce.plot.yAxisLabel: 

210 config = self.fluxes[self.mag_y] 

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

212 self.produce.plot.yAxisLabel = ( 

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

214 ) 

215 if self.unit is None: 

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

217 if self.name_prefix is None: 

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

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

220 if not self.produce.metric.units: 

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

222 

223 

224class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

226 

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

228 scale_factor = Field[float]( 

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

230 default=200, 

231 ) 

232 coord_label = Field[str]( 

233 doc="The plot label for the astrometric variable (default coord_meas)", 

234 optional=True, 

235 default=None, 

236 ) 

237 coord_meas = Field[str]( 

238 doc="The key for measured values of the astrometric variable", 

239 optional=False, 

240 ) 

241 coord_ref = Field[str]( 

242 doc="The key for reference values of the astrometric variabler", 

243 optional=False, 

244 ) 

245 

246 def finalize(self): 

247 # Check if it has already been finalized 

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

249 # Set before MagnitudeScatterPlot.finalize to undo PSF default. 

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

251 self._set_flux_default("mag_sn") 

252 super().finalize() 

253 name = self.coord_label if self.coord_label else self.coord_meas 

254 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.coord_meas) 

255 self.process.buildActions.pos_ref = LoadVector(vectorKey=self.coord_ref) 

256 if self.compute_chi: 

257 self.process.buildActions.diff = DivideVector( 

258 actionA=SubtractVector( 

259 actionA=self.process.buildActions.pos_meas, 

260 actionB=self.process.buildActions.pos_ref, 

261 ), 

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

263 ) 

264 else: 

265 self.process.buildActions.diff = MultiplyVector( 

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

267 actionB=SubtractVector( 

268 actionA=self.process.buildActions.pos_meas, 

269 actionB=self.process.buildActions.pos_ref, 

270 ), 

271 ) 

272 if self.unit is None: 

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

274 if self.name_prefix is None: 

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

276 self.name_prefix = f"astrom_{self.coord_meas}_{{name_class}}_{subtype}_" 

277 if not self.produce.metric.units: 

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

279 if not self.produce.plot.yAxisLabel: 

280 self.produce.plot.yAxisLabel = ( 

281 f"chi = (slot - reference {name} position)/error" 

282 if self.compute_chi 

283 else f"slot - reference {name} position ({self.unit})" 

284 )