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

131 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-22 11:50 +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 "ReferenceGalaxySelector", 

25 "ReferenceObjectSelector", 

26 "ReferenceStarSelector", 

27 "MatchedRefCoaddToolBase", 

28 "MatchedRefCoaddDiffTool", 

29 "MatchedRefCoaddDiffMagTool", 

30 "MatchedRefCoaddDiffPositionTool", 

31) 

32 

33from lsst.pex.config import Field 

34 

35from ..actions.vector import ( 

36 CalcBinnedStatsAction, 

37 ConstantValue, 

38 DivideVector, 

39 DownselectVector, 

40 LoadVector, 

41 MultiplyVector, 

42 SubtractVector, 

43) 

44from ..actions.vector.selectors import RangeSelector, ThresholdSelector, VectorSelector 

45from ..interfaces import KeyedData, Vector 

46from .genericBuild import MagnitudeXTool 

47from .genericProduce import MagnitudeScatterPlot 

48 

49 

50class ReferenceGalaxySelector(ThresholdSelector): 

51 """A selector that selects galaxies from a catalog with a 

52 boolean column identifying unresolved sources. 

53 """ 

54 

55 def __call__(self, data: KeyedData, **kwargs) -> Vector: 

56 result = super().__call__(data=data, **kwargs) 

57 if self.plotLabelKey: 

58 self._addValueToPlotInfo("true galaxies", **kwargs) 

59 return result 

60 

61 def setDefaults(self): 

62 super().setDefaults() 

63 self.op = "eq" 

64 self.threshold = 0 

65 self.plotLabelKey = "Selection: Galaxies" 

66 self.vectorKey = "refcat_is_pointsource" 

67 

68 

69class ReferenceObjectSelector(RangeSelector): 

70 """A selector that selects all objects from a catalog with a 

71 boolean column identifying unresolved sources. 

72 """ 

73 

74 def setDefaults(self): 

75 super().setDefaults() 

76 self.minimum = 0 

77 self.vectorKey = "refcat_is_pointsource" 

78 

79 

80class ReferenceStarSelector(ThresholdSelector): 

81 """A selector that selects stars from a catalog with a 

82 boolean column identifying unresolved sources. 

83 """ 

84 

85 def __call__(self, data: KeyedData, **kwargs) -> Vector: 

86 result = super().__call__(data=data, **kwargs) 

87 if self.plotLabelKey: 

88 self._addValueToPlotInfo("true stars", **kwargs) 

89 return result 

90 

91 def setDefaults(self): 

92 super().setDefaults() 

93 self.op = "eq" 

94 self.plotLabelKey = "Selection: Stars" 

95 self.threshold = 1 

96 self.vectorKey = "refcat_is_pointsource" 

97 

98 

99class MatchedRefCoaddToolBase(MagnitudeXTool): 

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

101 

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

103 require separate star/galaxy/all source selections. 

104 

105 Notes 

106 ----- 

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

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

109 appropriate candidates (and stores a match_candidate column). 

110 """ 

111 

112 def setDefaults(self): 

113 super().setDefaults() 

114 self.mag_x = "ref_matched" 

115 self.process.buildActions.allSelector = ReferenceObjectSelector() 

116 self.process.buildActions.galaxySelector = ReferenceGalaxySelector() 

117 self.process.buildActions.starSelector = ReferenceStarSelector() 

118 

119 def finalize(self): 

120 super().finalize() 

121 self._set_flux_default("mag_x") 

122 

123 

124class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase): 

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

126 

127 compute_chi = Field[bool]( 

128 default=False, 

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

130 ) 

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

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

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

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

135 

136 _mag_low_min: int = 15 

137 _mag_low_max: int = 27 

138 _mag_interval: int = 1 

139 

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

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

142 

143 def configureMetrics( 

144 self, 

145 unit: str | None = None, 

146 name_prefix: str | None = None, 

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

148 unit_select: str = "mag", 

149 ): 

150 """Configure metric actions and return units. 

151 

152 Parameters 

153 ---------- 

154 unit : `str` 

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

156 name_prefix : `str` 

157 The prefix for the action (column) name. 

158 name_suffix : `str` 

159 The sufffix for the action (column) name. 

160 unit_select : `str` 

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

162 

163 Returns 

164 ------- 

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

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

167 """ 

168 if unit is None: 

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

170 if name_prefix is None: 

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

172 

173 if unit_select is None: 

174 unit_select = "mag" 

175 

176 key_flux = self.config_mag_x.key_flux 

177 

178 units = {} 

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

180 name_capital = name.capitalize() 

181 x_key = f"x{name_capital}" 

182 

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

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

185 action.selector_range = RangeSelector( 

186 vectorKey=x_key, 

187 minimum=minimum, 

188 maximum=minimum + self._mag_interval, 

189 ) 

190 

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

192 if self.parameterizedBand: 

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

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

195 

196 units.update( 

197 { 

198 action.name_median: unit, 

199 action.name_sigmaMad: unit, 

200 action.name_count: "count", 

201 action.name_select_minimum: unit_select, 

202 action.name_select_median: unit_select, 

203 action.name_select_maximum: unit_select, 

204 } 

205 ) 

206 return units 

207 

208 def setDefaults(self): 

209 super().setDefaults() 

210 

211 self.process.filterActions.yAll = DownselectVector( 

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

213 ) 

214 self.process.filterActions.yGalaxies = DownselectVector( 

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

216 ) 

217 self.process.filterActions.yStars = DownselectVector( 

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

219 ) 

220 

221 for name in self._names: 

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

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

224 setattr( 

225 self.process.calculateActions, 

226 f"{name}{minimum}", 

227 CalcBinnedStatsAction( 

228 key_vector=key, 

229 selector_range=RangeSelector( 

230 vectorKey=key, 

231 minimum=minimum, 

232 maximum=minimum + self._mag_interval, 

233 ), 

234 ), 

235 ) 

236 

237 

238class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

240 

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

242 

243 def finalize(self): 

244 # Check if it has already been finalized 

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

246 # Ensure mag_y is set before any plot finalizes 

247 self._set_flux_default("mag_y") 

248 super().finalize() 

249 if self.compute_chi: 

250 self.process.buildActions.diff = DivideVector( 

251 actionA=SubtractVector( 

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

253 actionB=self.process.buildActions.flux_ref_matched, 

254 ), 

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

256 ) 

257 else: 

258 self.process.buildActions.diff = DivideVector( 

259 actionA=SubtractVector( 

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

261 actionB=self.process.buildActions.mag_ref_matched, 

262 ), 

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

264 ) 

265 if not self.produce.plot.yAxisLabel: 

266 config = self.fluxes[self.mag_y] 

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

268 self.produce.plot.yAxisLabel = ( 

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

270 ) 

271 if self.unit is None: 

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

273 if self.name_prefix is None: 

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

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

276 if not self.produce.metric.units: 

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

278 

279 

280class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

282 

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

284 scale_factor = Field[float]( 

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

286 default=200, 

287 ) 

288 coord_label = Field[str]( 

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

290 optional=True, 

291 default=None, 

292 ) 

293 coord_meas = Field[str]( 

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

295 optional=False, 

296 ) 

297 coord_ref = Field[str]( 

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

299 optional=False, 

300 ) 

301 

302 def finalize(self): 

303 # Check if it has already been finalized 

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

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

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

307 self._set_flux_default("mag_sn") 

308 super().finalize() 

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

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

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

312 if self.compute_chi: 

313 self.process.buildActions.diff = DivideVector( 

314 actionA=SubtractVector( 

315 actionA=self.process.buildActions.pos_meas, 

316 actionB=self.process.buildActions.pos_ref, 

317 ), 

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

319 ) 

320 else: 

321 self.process.buildActions.diff = MultiplyVector( 

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

323 actionB=SubtractVector( 

324 actionA=self.process.buildActions.pos_meas, 

325 actionB=self.process.buildActions.pos_ref, 

326 ), 

327 ) 

328 if self.unit is None: 

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

330 if self.name_prefix is None: 

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

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

333 if not self.produce.metric.units: 

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

335 if not self.produce.plot.yAxisLabel: 

336 self.produce.plot.yAxisLabel = ( 

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

338 if self.compute_chi 

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

340 )