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

217 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-13 11:47 +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 "MatchedRefCoaddDiffColorTool", 

30 "MatchedRefCoaddDiffMagTool", 

31 "MatchedRefCoaddDiffPositionTool", 

32) 

33 

34import copy 

35 

36from lsst.pex.config import DictField, Field 

37 

38from ..actions.vector import ( 

39 CalcBinnedStatsAction, 

40 ColorDiff, 

41 ColorError, 

42 ConstantValue, 

43 DivideVector, 

44 DownselectVector, 

45 LoadVector, 

46 MultiplyVector, 

47 SubtractVector, 

48) 

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

50from ..interfaces import KeyedData, Vector 

51from .genericBuild import MagnitudeXTool 

52from .genericMetricAction import StructMetricAction 

53from .genericPlotAction import StructPlotAction 

54from .genericProduce import MagnitudeScatterPlot 

55 

56 

57class ReferenceGalaxySelector(ThresholdSelector): 

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

59 boolean column identifying unresolved sources. 

60 """ 

61 

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

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

64 if self.plotLabelKey: 

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

66 return result 

67 

68 def setDefaults(self): 

69 super().setDefaults() 

70 self.op = "eq" 

71 self.threshold = 0 

72 self.plotLabelKey = "Selection: Galaxies" 

73 self.vectorKey = "refcat_is_pointsource" 

74 

75 

76class ReferenceObjectSelector(RangeSelector): 

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

78 boolean column identifying unresolved sources. 

79 """ 

80 

81 def setDefaults(self): 

82 super().setDefaults() 

83 self.minimum = 0 

84 self.vectorKey = "refcat_is_pointsource" 

85 

86 

87class ReferenceStarSelector(ThresholdSelector): 

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

89 boolean column identifying unresolved sources. 

90 """ 

91 

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

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

94 if self.plotLabelKey: 

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

96 return result 

97 

98 def setDefaults(self): 

99 super().setDefaults() 

100 self.op = "eq" 

101 self.plotLabelKey = "Selection: Stars" 

102 self.threshold = 1 

103 self.vectorKey = "refcat_is_pointsource" 

104 

105 

106class MatchedRefCoaddToolBase(MagnitudeXTool): 

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

108 

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

110 require separate star/galaxy/all source selections. 

111 

112 Notes 

113 ----- 

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

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

116 appropriate candidates (and stores a match_candidate column). 

117 """ 

118 

119 def setDefaults(self): 

120 super().setDefaults() 

121 self.mag_x = "ref_matched" 

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

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

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

125 

126 def finalize(self): 

127 super().finalize() 

128 self._set_flux_default("mag_x") 

129 

130 

131class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase): 

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

133 

134 compute_chi = Field[bool]( 

135 default=False, 

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

137 ) 

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

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

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

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

142 

143 _mag_low_min: int = 15 

144 _mag_low_max: int = 27 

145 _mag_interval: int = 1 

146 

147 _names = {"stars": "star", "galaxies": "galaxy", "all": "all"} 

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

149 

150 def _set_actions(self, suffix=None): 

151 if suffix is None: 

152 suffix = "" 

153 

154 for name_lower, name_singular in self._names.items(): 

155 name = name_lower.capitalize() 

156 key = f"y{name}{suffix}" 

157 setattr( 

158 self.process.filterActions, 

159 key, 

160 DownselectVector( 

161 vectorKey=f"diff{suffix}", selector=VectorSelector(vectorKey=f"{name_singular}Selector") 

162 ), 

163 ) 

164 

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

166 setattr( 

167 self.process.calculateActions, 

168 f"{name_lower}{minimum}{suffix}", 

169 CalcBinnedStatsAction( 

170 key_vector=key, 

171 selector_range=RangeSelector( 

172 vectorKey=key, 

173 minimum=minimum, 

174 maximum=minimum + self._mag_interval, 

175 ), 

176 return_minmax=False, 

177 ), 

178 ) 

179 

180 def configureMetrics( 

181 self, 

182 unit: str | None = None, 

183 name_prefix: str | None = None, 

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

185 attr_suffix: str | None = None, 

186 unit_select: str = "mag", 

187 ): 

188 """Configure metric actions and return units. 

189 

190 Parameters 

191 ---------- 

192 unit : `str` 

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

194 name_prefix : `str` 

195 The prefix for the action (column) name. 

196 name_suffix : `str` 

197 The suffix for the action (column) name. 

198 attr_suffix : `str` 

199 The suffix for the attribute to assign the action to. 

200 unit_select : `str` 

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

202 

203 Returns 

204 ------- 

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

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

207 """ 

208 if unit is None: 

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

210 if name_prefix is None: 

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

212 if attr_suffix is None: 

213 attr_suffix = "" 

214 

215 if unit_select is None: 

216 unit_select = "mag" 

217 

218 key_flux = self.config_mag_x.key_flux 

219 

220 units = {} 

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

222 name_capital = name.capitalize() 

223 x_key = f"x{name_capital}" 

224 

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

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

227 action.selector_range = RangeSelector( 

228 vectorKey=x_key, 

229 minimum=minimum, 

230 maximum=minimum + self._mag_interval, 

231 ) 

232 

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

234 if self.parameterizedBand: 

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

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

237 

238 units.update( 

239 { 

240 action.name_median: unit, 

241 action.name_sigmaMad: unit, 

242 action.name_count: "count", 

243 action.name_select_median: unit_select, 

244 } 

245 ) 

246 return units 

247 

248 def setDefaults(self): 

249 super().setDefaults() 

250 self._set_actions() 

251 

252 

253class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

255 

256 Notes 

257 ----- 

258 Since this tool requires at least two bands, it is essentially impossible 

259 to call on its own. 

260 """ 

261 

262 mag_y1 = Field[str](default="cmodel_err", doc="Flux field for first magnitude") 

263 mag_y2 = Field[str]( 

264 doc="Flux field for second magnitude (to subtract from first); default same as first", 

265 default=None, 

266 optional=True, 

267 ) 

268 bands = DictField[str, str]( 

269 doc="Bands for colors. ", 

270 default={"u": "g", "g": "r,i", "r": "i", "i": "z", "z": "y"}, 

271 ) 

272 band_separator = Field[str](default=",", doc="Separator for multiple bands") 

273 

274 def _split_bands(self, band_list: str): 

275 return band_list.split(self.band_separator) 

276 

277 def finalize(self): 

278 # Check if it has already been finalized 

279 if not hasattr(self.process.buildActions, "diff_0"): 

280 if self.mag_y2 is None: 

281 self.mag_y2 = self.mag_y1 

282 # Ensure mag_y1/2 are set before any plot finalizes 

283 bands = {band1: self._split_bands(band2_list) for band1, band2_list in self.bands.items()} 

284 n_bands = 0 

285 for band1, band2_list in bands.items(): 

286 for band2 in band2_list: 

287 mag1 = f"y_{band1}" 

288 mag2 = f"y_{band2}" 

289 mag_ref1 = f"refcat_flux_{band1}" 

290 mag_ref2 = f"refcat_flux_{band2}" 

291 self._set_flux_default(mag1, band=band1, name_mag=self.mag_y1) 

292 self._set_flux_default(mag2, band=band2, name_mag=self.mag_y2) 

293 self._set_flux_default(mag_ref1, band=band1, name_mag="ref_matched") 

294 self._set_flux_default(mag_ref2, band=band2, name_mag="ref_matched") 

295 n_bands += 1 

296 

297 self.suffixes_y_finalize = [f"_{idx}" for idx in range(n_bands)] 

298 super().finalize() 

299 

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

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

302 

303 metric_base = self.produce.metric 

304 plot_base = self.produce.plot 

305 

306 actions_metric = {} 

307 actions_plot = {} 

308 

309 idx = 0 

310 for band1, band2_list in bands.items(): 

311 for band2 in band2_list: 

312 name_color = f"{band1}_minus_{band2}" 

313 # Keep this index-based to simplify finalize 

314 suffix_y = f"_{idx}" 

315 self._set_actions(suffix=suffix_y) 

316 metric = copy.copy(metric_base) 

317 self.name_prefix = f"photom_mag_{{key_flux}}_color_{name_color}_{{name_class}}_{subtype}_" 

318 metric.units = self.configureMetrics(attr_suffix=suffix_y) 

319 plot = copy.copy(plot_base) 

320 

321 plot.suffix_y = suffix_y 

322 plot.suffix_stat = suffix_y 

323 

324 mag1 = f"{self.mag_y1}_{band1}" 

325 mag2 = f"{self.mag_y2}_{band2}" 

326 mag_ref1 = f"ref_matched_{band1}" 

327 mag_ref2 = f"ref_matched_{band2}" 

328 

329 diff = ColorDiff( 

330 color1_flux1=getattr(self.process.buildActions, f"flux_{mag1}"), 

331 color1_flux2=getattr(self.process.buildActions, f"flux_{mag2}"), 

332 color2_flux1=getattr(self.process.buildActions, f"flux_{mag_ref1}"), 

333 color2_flux2=getattr(self.process.buildActions, f"flux_{mag_ref2}"), 

334 ) 

335 

336 if self.compute_chi: 

337 diff = DivideVector( 

338 actionA=diff, 

339 actionB=ColorError( 

340 flux_err1=DivideVector( 

341 actionA=getattr(self.process.buildActions, f"flux_err_{mag1}"), 

342 actionB=getattr(self.process.buildActions, f"flux_{mag1}"), 

343 ), 

344 flux_err2=DivideVector( 

345 actionA=getattr(self.process.buildActions, f"flux_err_{mag2}"), 

346 actionB=getattr(self.process.buildActions, f"flux_{mag2}"), 

347 ), 

348 ), 

349 ) 

350 setattr(self.process.buildActions, f"diff{plot.suffix_y}", diff) 

351 

352 label = f"({band1} - {band2}) (meas - ref)" 

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

354 plot.yAxisLabel = label 

355 actions_metric[name_color] = metric 

356 actions_plot[name_color] = plot 

357 idx += 1 

358 action_metric = StructMetricAction() 

359 for name_action, action in actions_metric.items(): 

360 setattr(action_metric.actions, name_action, action) 

361 self.produce.metric = action_metric 

362 action_plot = StructPlotAction() 

363 for name_action, action in actions_plot.items(): 

364 setattr(action_plot.actions, name_action, action) 

365 self.produce.plot = action_plot 

366 

367 def setDefaults(self): 

368 # skip MatchedRefCoaddDiffTool.setDefaults's _setActions call 

369 MatchedRefCoaddToolBase.setDefaults(self) 

370 MagnitudeScatterPlot.setDefaults(self) 

371 

372 def validate(self): 

373 super().validate() 

374 errors = [] 

375 for band1, band2_list in self.bands.items(): 

376 bands = self._split_bands(band2_list) 

377 if len(set(bands)) != len(bands): 

378 errors.append(f"value={band2_list} is not a set for key={band1}") 

379 if errors: 

380 raise ValueError("\n".join(errors)) 

381 

382 

383class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

385 

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

387 

388 def finalize(self): 

389 # Check if it has already been finalized 

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

391 # Ensure mag_y is set before any plot finalizes 

392 self._set_flux_default("mag_y") 

393 super().finalize() 

394 if self.compute_chi: 

395 self.process.buildActions.diff = DivideVector( 

396 actionA=SubtractVector( 

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

398 actionB=self.process.buildActions.flux_ref_matched, 

399 ), 

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

401 ) 

402 else: 

403 self.process.buildActions.diff = DivideVector( 

404 actionA=SubtractVector( 

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

406 actionB=self.process.buildActions.mag_ref_matched, 

407 ), 

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

409 ) 

410 if not self.produce.plot.yAxisLabel: 

411 config = self.fluxes[self.mag_y] 

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

413 self.produce.plot.yAxisLabel = ( 

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

415 ) 

416 if self.unit is None: 

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

418 if self.name_prefix is None: 

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

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

421 if not self.produce.metric.units: 

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

423 

424 

425class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

427 

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

429 scale_factor = Field[float]( 

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

431 default=200, 

432 ) 

433 coord_label = Field[str]( 

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

435 optional=True, 

436 default=None, 

437 ) 

438 coord_meas = Field[str]( 

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

440 optional=False, 

441 ) 

442 coord_ref = Field[str]( 

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

444 optional=False, 

445 ) 

446 

447 def finalize(self): 

448 # Check if it has already been finalized 

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

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

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

452 self._set_flux_default("mag_sn") 

453 super().finalize() 

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

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

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

457 if self.compute_chi: 

458 self.process.buildActions.diff = DivideVector( 

459 actionA=SubtractVector( 

460 actionA=self.process.buildActions.pos_meas, 

461 actionB=self.process.buildActions.pos_ref, 

462 ), 

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

464 ) 

465 else: 

466 self.process.buildActions.diff = MultiplyVector( 

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

468 actionB=SubtractVector( 

469 actionA=self.process.buildActions.pos_meas, 

470 actionB=self.process.buildActions.pos_ref, 

471 ), 

472 ) 

473 if self.unit is None: 

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

475 if self.name_prefix is None: 

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

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

478 if not self.produce.metric.units: 

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

480 if not self.produce.plot.yAxisLabel: 

481 self.produce.plot.yAxisLabel = ( 

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

483 if self.compute_chi 

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

485 )