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

543 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-15 00:23 +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 "MatchedRefCoaddTool", 

25 "MatchedRefCoaddChiColorTool", 

26 "MatchedRefCoaddChiCoordDecTool", 

27 "MatchedRefCoaddChiCoordRaTool", 

28 "MatchedRefCoaddChiDistanceTool", 

29 "MatchedRefCoaddChiMagTool", 

30 "MatchedRefCoaddCompurityTool", 

31 "MatchedRefCoaddDiffColorTool", 

32 "MatchedRefCoaddDiffColorZoomTool", 

33 "MatchedRefCoaddDiffCoordDecTool", 

34 "MatchedRefCoaddDiffCoordDecZoomTool", 

35 "MatchedRefCoaddDiffCoordRaTool", 

36 "MatchedRefCoaddDiffCoordRaZoomTool", 

37 "MatchedRefCoaddDiffDistanceTool", 

38 "MatchedRefCoaddDiffMagTool", 

39 "MatchedRefCoaddDiffMagZoomTool", 

40 "MatchedRefCoaddDiffPositionTool", 

41 "MatchedRefCoaddDiffTool", 

42 "MatchedRefCoaddDiffDistanceZoomTool", 

43 "reconfigure_diff_matched_defaults", 

44) 

45 

46import copy 

47import inspect 

48from abc import abstractmethod 

49 

50import astropy.units as u 

51import lsst.pex.config as pexConfig 

52from lsst.pex.config import DictField, Field 

53from lsst.pex.config.configurableActions import ConfigurableActionField 

54from lsst.utils.plotting.publication_plots import galaxies_color, stars_color 

55 

56from ..actions.config import MagnitudeBinConfig 

57from ..actions.keyedData import ( 

58 CalcBinnedCompletenessAction, 

59 CalcCompletenessHistogramAction, 

60 MagnitudeCompletenessConfig, 

61) 

62from ..actions.plot import CompletenessHist 

63from ..actions.vector import ( 

64 CalcBinnedStatsAction, 

65 ColorDiff, 

66 ColorError, 

67 ConstantValue, 

68 CosVector, 

69 DivideVector, 

70 DownselectVector, 

71 IsMatchedObjectSameClass, 

72 LoadVector, 

73 MultiplyVector, 

74 SubtractVector, 

75) 

76from ..actions.vector.selectors import ( 

77 InjectedGalaxySelector, 

78 InjectedObjectSelector, 

79 InjectedStarSelector, 

80 MatchedObjectSelector, 

81 RangeSelector, 

82 ReferenceGalaxySelector, 

83 ReferenceObjectSelector, 

84 ReferenceStarSelector, 

85 SelectorBase, 

86 VectorSelector, 

87) 

88from ..interfaces import AnalysisBaseConfig, BaseMetricAction, NoMetric 

89from .genericBuild import MagnitudeTool, MagnitudeXTool, ObjectClassTool 

90from .genericMetricAction import StructMetricAction 

91from .genericPlotAction import StructPlotAction 

92from .genericProduce import MagnitudeScatterPlot 

93 

94 

95def _set_field_config(config: pexConfig.Config | pexConfig.ConfigMeta, name: str, value): 

96 """Set the value of a Config Field or ConfigMeta default value. 

97 

98 Parameters 

99 ---------- 

100 config 

101 A Config instance or metaclass. 

102 name 

103 The name of the attribute to set. 

104 value 

105 The value to set it to. 

106 """ 

107 if isinstance(config, pexConfig.ConfigMeta): 

108 getattr(config, name).default = value 

109 else: 

110 setattr(config, name, value) 

111 

112 

113class MatchedRefCoaddTool(ObjectClassTool): 

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

115 

116 This tool is designed to configure plots and metrics as a function of 

117 magnitude (object or reference). The metrics are binned by the same 

118 magnitude shown on the x-axis in plots. By default, this is the reference 

119 magnitude but plots can be configured to bin by object magnitude instead. 

120 

121 Notes 

122 ----- 

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

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

125 appropriate candidates (and stores a match_candidate column). 

126 

127 The tool requires specification of reference galaxy and star selectors, 

128 as these will be used to determine whether matched objects have the same 

129 class as the reference, even if a particular class is not being plotted. 

130 It is okay to specify a "dummy" selector that always returns False if 

131 there are no reference objects of the given class. 

132 """ 

133 

134 _suffix_ref = "_ref" 

135 _suffix_target = "_target" 

136 

137 context = pexConfig.ChoiceField[str]( 

138 doc="The context for the selectors", 

139 allowed={ 

140 "custom": "User-configured selectors", 

141 "DC2": "DC2 Truth Summary match", 

142 "injection": "Source injection match", 

143 }, 

144 default="DC2", 

145 ) 

146 

147 select_ref_by_default = pexConfig.Field[bool]( 

148 doc="Whether reference quantities should be used by default in other tools," 

149 " e.g. for binning metrics and for the x-axis in plots", 

150 default=True, 

151 ) 

152 

153 selector_ref_all = ConfigurableActionField[SelectorBase]( 

154 doc="The selector for reference objects of all types", 

155 default=ReferenceObjectSelector, 

156 ) 

157 selector_ref_galaxy = ConfigurableActionField[SelectorBase]( 

158 doc="The selector for reference galaxies", 

159 default=ReferenceGalaxySelector, 

160 ) 

161 selector_ref_star = ConfigurableActionField[SelectorBase]( 

162 doc="The selector for reference stars", 

163 default=ReferenceStarSelector, 

164 ) 

165 

166 mag_bins = pexConfig.ConfigField[MagnitudeBinConfig](doc="Magnitude bin configuration for metrics") 

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

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

169 name_prefix = pexConfig.Field[str]( 

170 doc="Default prefix for metric key. Can include {name_type} as a" 

171 " template for the type of object (resolved/unresolved)", 

172 default=None, 

173 optional=True, 

174 ) 

175 name_suffix = pexConfig.Field[str]( 

176 doc="The suffix for metric names. Can include {name_mag} as a " 

177 " template for the magnitude algorithm", 

178 default="_ref_mag{name_mag}", 

179 ) 

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

181 

182 def finalize(self): 

183 # Don't do anything if the value is the one for which the defaults of 

184 # selector_ref_all, etc are - this can't easily be inferred and must 

185 # be kept in sync manually 

186 if self.context != "DC2": 

187 match self.context: 

188 case "injection": 

189 self.selector_ref_all = InjectedObjectSelector() 

190 self.selector_ref_galaxy = InjectedGalaxySelector() 

191 self.selector_ref_star = InjectedStarSelector() 

192 case "custom": 

193 pass 

194 case _: 

195 raise NotImplementedError(f"{self.context=} is not implemented in {self.__class__}") 

196 

197 # Other tools will except selector_all 

198 self.selection_suffix = self._suffix_ref if self.select_ref_by_default else self._suffix_target 

199 

200 super().finalize() 

201 

202 for object_class in self.get_classes(): 

203 name_selector = self.get_name_attr_selector(object_class, self._suffix_ref) 

204 selector = self.get_selector_ref(object_class) 

205 # This is a build action because selectors in prep are applied with 

206 # and; we're not using these to filter all points but to make 

207 # several parallel selections 

208 setattr(self.process.buildActions, name_selector, selector) 

209 

210 def get_selector_ref(self, object_class: str): 

211 match object_class: 

212 case "any": 

213 return self.selector_ref_all 

214 case "galaxy": 

215 return self.selector_ref_galaxy 

216 case "star": 

217 return self.selector_ref_star 

218 

219 def reconfigure( 

220 self, 

221 context: str | None = None, 

222 key_flux_meas: str | None = None, 

223 bands_color: dict[str, str] | list[str] | None = None, 

224 use_any: bool | None = None, 

225 use_galaxies: bool | None = None, 

226 use_stars: bool | None = None, 

227 ): 

228 """Reconfigure any MatchedRefCoaddTools in an analysis task config. 

229 

230 Parameters 

231 ---------- 

232 context 

233 The context to set. Must be a valid choice for 

234 MatchedRefCoaddTool.context. 

235 key_flux_meas 

236 The key of the measured flux config to use, e.g. "psf". If the key 

237 is not found, it will search for f"{key}_err", the default name for 

238 configurations that load error keys as well as fluxes. 

239 bands_color 

240 A dictionary keyed by band of comma-separated bands to measure 

241 colors for, where the color is (key - value). If a list is passed, 

242 tools will modify the defaults to select only those bands within 

243 the list (which should also be a set). 

244 use_any 

245 Whether to compute metrics for objects of all types. 

246 use_galaxies 

247 Whether to compute metrics and plot lines for galaxies only. 

248 use_stars 

249 Whether to compute metrics and plot lines for stars only. 

250 

251 Notes 

252 ----- 

253 Any kwargs set to None will not change the relevant config fields. 

254 """ 

255 if context is not None: 

256 _set_field_config(self, name="context", value=context) 

257 if use_any is not None: 

258 _set_field_config(self, name="use_any", value=use_any) 

259 if use_galaxies is not None: 

260 _set_field_config(self, name="use_galaxies", value=use_galaxies) 

261 if use_stars is not None: 

262 _set_field_config(self, name="use_stars", value=use_stars) 

263 

264 # This allows the method to work automatically on class defaults 

265 kwargs = {"self": self} if inspect.isclass(self) else {} 

266 

267 # Change any dependent magnitudes 

268 self.reconfigure_dependent_magnitudes(key_flux_meas=key_flux_meas, bands_color=bands_color, **kwargs) 

269 

270 def reconfigure_dependent_magnitudes( 

271 self, 

272 key_flux_meas: str | None = None, 

273 bands_color: dict[str, str] | list[str] | None = None, 

274 ): 

275 """Reconfigure any dependent (i.e., on the y-axis in plots) magnitude 

276 column configs. 

277 

278 Parameters 

279 ---------- 

280 key_flux_meas 

281 The key of the measured flux config to set to. 

282 bands_color 

283 A dictionary keyed by band of comma-separated bands to measure 

284 colors for, where the color is (key - value). If a list is passed, 

285 tools will modify the defaults to select only those bands within 

286 the list (which should also be a set). 

287 """ 

288 

289 def setDefaults(self): 

290 super().setDefaults() 

291 # The selection info isn't useful in plots with multiple classes 

292 self.selector_ref_galaxy.plotLabelKey = None 

293 self.selector_ref_star.plotLabelKey = None 

294 

295 

296class MatchedRefCoaddDiffTool(MagnitudeXTool, MatchedRefCoaddTool): 

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

298 

299 limits_chi_default = (-5, 5) 

300 limits_diff_color_mmag_default = (-250.0, 250.0) 

301 limits_diff_color_mmag_zoom_default = (-50.0, 50.0) 

302 limits_diff_mag_mmag_default = (-1000.0, 1000.0) 

303 limits_diff_mag_mmag_zoom_default = (-50.0, 50.0) 

304 limits_diff_pos_mas_default = (-500, 500) 

305 limits_diff_pos_mas_zoom_default = (-10, 10) 

306 limits_x_mag_default = (16.5, 29.0) 

307 limits_x_mag_zoom_default = (16.5, 24.0) 

308 

309 compute_chi = pexConfig.Field[bool]( 

310 default=False, 

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

312 ) 

313 

314 def _set_actions(self, suffix=None): 

315 if suffix is None: 

316 suffix = "" 

317 

318 selection = self._suffix_ref if self.select_ref_by_default else self._suffix_target 

319 for object_class in self.get_classes(): 

320 name_type_plural = self.get_class_name_plural(object_class) 

321 name_attr = f"{self.get_name_attr_values(object_class)}{suffix}" 

322 name_selector = self.get_name_attr_selector(object_class, selection) 

323 name_x = f"x{name_type_plural.capitalize()}" 

324 

325 y_values = DownselectVector( 

326 vectorKey=f"diff{suffix}", 

327 selector=VectorSelector(vectorKey=name_selector), 

328 ) 

329 setattr(self.process.filterActions, name_attr, y_values) 

330 

331 bins = self.mag_bins.get_bins() 

332 for minimum in bins: 

333 setattr( 

334 self.process.calculateActions, 

335 f"{name_type_plural}_{minimum}{suffix}", 

336 CalcBinnedStatsAction( 

337 key_vector=name_attr, 

338 selector_range=RangeSelector( 

339 vectorKey=name_x, 

340 minimum=minimum, 

341 maximum=minimum + self.mag_bins.mag_width, 

342 ), 

343 ), 

344 ) 

345 

346 def configureMetrics( 

347 self, 

348 unit: str | None = None, 

349 name_prefix: str | None = None, 

350 attr_suffix: str | None = None, 

351 unit_select: str = "mag", 

352 ): 

353 """Configure metric actions and return units. 

354 

355 Parameters 

356 ---------- 

357 unit : `str` 

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

359 name_prefix : `str` 

360 The prefix for the action (column) name. 

361 attr_suffix : `str` 

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

363 unit_select : `str` 

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

365 

366 Returns 

367 ------- 

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

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

370 """ 

371 if unit is None: 

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

373 if name_prefix is None: 

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

375 if attr_suffix is None: 

376 attr_suffix = "" 

377 

378 if unit_select is None: 

379 unit_select = "mag" 

380 

381 key_flux = self.config_mag_x.key_flux 

382 

383 units = {} 

384 

385 for object_class in self.get_classes(): 

386 name_type = self.get_class_type(object_class) 

387 name_type_plural = self.get_class_name_plural(object_class) 

388 name_capital = name_type_plural.capitalize() 

389 x_key = f"x{name_capital}" 

390 

391 # Set up metrics for objects of one class within a magnitude range 

392 bins = self.mag_bins.get_bins() 

393 for minimum in bins: 

394 action = getattr(self.process.calculateActions, f"{name_type_plural}_{minimum}{attr_suffix}") 

395 action.selector_range = RangeSelector( 

396 vectorKey=x_key, 

397 minimum=minimum / 1000.0, 

398 maximum=(minimum + self.mag_bins.mag_width) / 1000.0, 

399 ) 

400 name_mag = self.mag_bins.get_name_bin(minimum) 

401 

402 action.name_prefix = name_prefix.format( 

403 key_flux=key_flux, 

404 name_type=name_type, 

405 ) 

406 if self.parameterizedBand: 

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

408 action.name_suffix = self.name_suffix.format(name_mag=name_mag) 

409 

410 units.update( 

411 { 

412 action.name_median: unit, 

413 action.name_sigmaMad: unit, 

414 action.name_count: "count", 

415 action.name_select_median: unit_select, 

416 } 

417 ) 

418 return units 

419 

420 @property 

421 def config_mag_y(self): 

422 """Return the y-axis magnitude config. 

423 

424 Although tools may not end up using any flux measures in metrics or 

425 plots, this should still be set to the flux measure that was matched 

426 or selected against in the catalog not used for the x-axis.""" 

427 mag_y = self.get_key_flux_y() 

428 if mag_y not in self.fluxes: 

429 raise KeyError(f"{mag_y=} not in {self.fluxes}; was finalize called?") 

430 # This is a logic error: it shouldn't be called before finalize 

431 assert mag_y in self.fluxes 

432 return self.fluxes[mag_y] 

433 

434 def finalize(self): 

435 MagnitudeXTool.finalize(self) 

436 MatchedRefCoaddTool.finalize(self) 

437 

438 @abstractmethod 

439 def get_key_flux_y(self) -> str: 

440 """Return the key for the y-axis flux measure.""" 

441 raise NotImplementedError("subclasses must implement get_key_flux_y") 

442 

443 def setDefaults(self): 

444 MagnitudeXTool.setDefaults(self) 

445 MatchedRefCoaddTool.setDefaults(self) 

446 self.mag_x = "ref_matched" 

447 self.prep.selectors.matched = MatchedObjectSelector() 

448 

449 

450class MatchedRefCoaddDiffPlot(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

451 """Base tool for generic diffs between reference and measured values, 

452 with a scatter plot.""" 

453 

454 def do_metrics(self): 

455 return not isinstance(self.produce.metric, NoMetric) 

456 

457 def get_key_flux_y(self) -> str: 

458 return super().get_key_flux_y() 

459 

460 def finalize(self): 

461 MatchedRefCoaddDiffTool.finalize(self) 

462 MagnitudeScatterPlot.finalize(self) 

463 

464 def setDefaults(self): 

465 # This will set no plot 

466 MatchedRefCoaddDiffTool.setDefaults(self) 

467 # This will set the plot 

468 MagnitudeScatterPlot.setDefaults(self) 

469 self.produce.plot.xLims = self.limits_x_mag_default 

470 

471 

472class MatchedRefCoaddCompurityTool(MagnitudeTool, MatchedRefCoaddTool): 

473 """Plot the fraction of injected sources recovered by input magnitude. 

474 

475 By contrast with MatchedRefCoaddDiffTool, where one must choose which 

476 magnitude appears on the x-axis, this tools creates two plots with 

477 different magnitudes. The completeness plot necessarily is a function 

478 of reference magnitude while purity is a function of object (target) 

479 magnitude. 

480 """ 

481 

482 config_metrics = pexConfig.ConfigField[MagnitudeCompletenessConfig]( 

483 doc="Plot-based (unbinned) metric definition configuration" 

484 ) 

485 key_match_distance = pexConfig.Field[str]( 

486 default="match_distance", 

487 doc="Key for match distance column (>=0 for a successful match)", 

488 ) 

489 mag_bins_plot = pexConfig.ConfigField[MagnitudeBinConfig]( 

490 doc="Magnitude bin configuration for plots and for unbinned metrics" 

491 "(including completeness at magnitude thresholds)" 

492 ) 

493 mag_ref = pexConfig.Field[str]( 

494 default="ref_matched", 

495 doc="Flux (magnitude) config key (to self.fluxes) for reference (true) magnitudes", 

496 ) 

497 mag_target = pexConfig.Field[str]( 

498 default="cmodel_err", 

499 doc="Flux (magnitude) config key (to self.fluxes) for target (measured) magnitudes", 

500 ) 

501 make_plots = pexConfig.Field[bool]( 

502 default=True, 

503 doc="Whether to generate plots in addition to metrics", 

504 ) 

505 

506 @property 

507 def config_mag_ref(self): 

508 return self._config_mag("mag_ref") 

509 

510 @property 

511 def config_mag_target(self): 

512 return self._config_mag("mag_target") 

513 

514 def finalize(self): 

515 if not self.produce.metric.units: 

516 MagnitudeTool.finalize(self) 

517 MatchedRefCoaddTool.finalize(self) 

518 self._set_flux_default("mag_ref") 

519 self._set_flux_default("mag_target") 

520 

521 # This is the default convention for metric names, originally set 

522 # for DC2 truth match but expanded to generic reference catalogs 

523 # (including injection catalogs) 

524 name_prefix = ( 

525 self.name_prefix 

526 if self.name_prefix 

527 else ( 

528 f"detect_{self.config_mag_target.name_flux_short}_vs_" 

529 f"{self.config_mag_ref.name_flux_short}_{{name_type}}_" 

530 ) 

531 ) 

532 unit_select = "" 

533 kwargs_matched_class_action = {} 

534 

535 # Set up selectors for all object classes as they may be needed by 

536 # the wrong/right matched class selector 

537 for object_class in ("any", "galaxy", "star"): 

538 for suffix, func_selector in ( 

539 (self._suffix_ref, self.get_selector_ref), 

540 (self._suffix_target, self.get_selector), 

541 ): 

542 name_selector = self.get_name_attr_selector(object_class, suffix) 

543 if not hasattr(self.process.buildActions, name_selector): 

544 selector = func_selector(object_class) 

545 setattr(self.process.buildActions, name_selector, selector) 

546 if object_class != "any": 

547 kwargs_matched_class_action[f"key_is{suffix}_{object_class}"] = name_selector 

548 

549 # This isn't exactly a filterAction but by default it needs to go 

550 # after build and before calc, so here it is 

551 self.process.filterActions.matched_class = IsMatchedObjectSameClass(**kwargs_matched_class_action) 

552 

553 key_flux = self.config_mag_ref.key_flux 

554 key_mag_ref = f"mag_{self.mag_ref}" 

555 key_mag_target = f"mag_{self.mag_target}" 

556 object_classes = self.get_classes() 

557 self.produce.metric = StructMetricAction() 

558 if self.make_plots: 

559 self.produce.plot = StructPlotAction() 

560 

561 for object_class in object_classes: 

562 name_type = self.get_class_type(object_class) 

563 name_selector_ref = self.get_name_attr_selector(object_class, self._suffix_ref) 

564 name_selector_target = self.get_name_attr_selector(object_class, self._suffix_target) 

565 name_prefix_class = name_prefix.format( 

566 key_flux=key_flux, 

567 name_type=name_type, 

568 ) 

569 if self.parameterizedBand: 

570 name_prefix_class = f"{{band}}_{name_prefix_class}" 

571 

572 units = {} 

573 completeness_binned_metrics = CalcCompletenessHistogramAction( 

574 action=CalcBinnedCompletenessAction( 

575 name_prefix=name_prefix_class, 

576 selector_range_ref=RangeSelector(vectorKey=key_mag_ref), 

577 selector_range_target=RangeSelector(vectorKey=key_mag_target), 

578 key_mask_ref=name_selector_ref, 

579 key_mask_target=name_selector_target, 

580 ), 

581 bins=self.mag_bins, 

582 ) 

583 # Metric bins should be coarser than plot bins and therefore 

584 # are unsuited for computing unbinned metrics (like mag at a 

585 # given completeness/purity) 

586 completeness_binned_metrics.config_metrics.completeness_percentiles = [] 

587 setattr( 

588 self.process.calculateActions, 

589 f"completeness_binned_metrics_{object_class}", 

590 completeness_binned_metrics, 

591 ) 

592 

593 bins = self.mag_bins.get_bins() 

594 for minimum in bins: 

595 name_mag = self.mag_bins.get_name_bin(minimum) 

596 action = CalcBinnedCompletenessAction( 

597 name_prefix=name_prefix_class, 

598 name_suffix=self.name_suffix.format(name_mag=name_mag), 

599 selector_range_ref=RangeSelector( 

600 vectorKey=key_mag_ref, 

601 minimum=minimum / 1000.0, 

602 maximum=(minimum + self.mag_bins.mag_width) / 1000.0, 

603 ), 

604 selector_range_target=RangeSelector( 

605 vectorKey=key_mag_target, 

606 minimum=minimum / 1000.0, 

607 maximum=(minimum + self.mag_bins.mag_width) / 1000.0, 

608 ), 

609 key_mask_ref=name_selector_ref, 

610 key_mask_target=name_selector_target, 

611 ) 

612 setattr( 

613 self.process.calculateActions, 

614 f"completeness_{object_class}_{minimum}", 

615 action, 

616 ) 

617 

618 units.update( 

619 { 

620 action.name_count: "count", 

621 action.name_count_ref: "count", 

622 action.name_count_target: "count", 

623 action.name_completeness: unit_select, 

624 action.name_completeness_bad_match: unit_select, 

625 action.name_completeness_good_match: unit_select, 

626 action.name_purity: unit_select, 

627 action.name_purity_bad_match: unit_select, 

628 action.name_purity_good_match: unit_select, 

629 } 

630 ) 

631 

632 completeness_plot = CalcCompletenessHistogramAction( 

633 action=CalcBinnedCompletenessAction( 

634 name_prefix=name_prefix_class, 

635 selector_range_ref=RangeSelector(vectorKey=key_mag_ref), 

636 selector_range_target=RangeSelector(vectorKey=key_mag_target), 

637 key_mask_ref=name_selector_ref, 

638 key_mask_target=name_selector_target, 

639 ), 

640 bins=self.mag_bins_plot, 

641 config_metrics=self.config_metrics, 

642 ) 

643 setattr( 

644 self.process.calculateActions, 

645 f"completeness_plot_{object_class}", 

646 completeness_plot, 

647 ) 

648 for pct in completeness_plot.config_metrics.completeness_percentiles: 

649 name_pct = completeness_plot.action.name_mag_completeness( 

650 completeness_plot.getPercentileName(pct) 

651 ) 

652 units[name_pct] = unit_select 

653 

654 # Make the metric action for the given object class 

655 # This will include units for metrics from the plot histogram 

656 # (i.e. the magnitude for a given completeness threshold) 

657 setattr( 

658 self.produce.metric.actions, 

659 object_class, 

660 BaseMetricAction(units=units), 

661 ) 

662 

663 if self.make_plots: 

664 overrides = {} 

665 if name_type == self.type_galaxies: 

666 overrides["color_counts"] = galaxies_color() 

667 elif name_type == self.type_stars: 

668 overrides["color_counts"] = stars_color() 

669 setattr( 

670 self.produce.plot.actions, 

671 object_class, 

672 CompletenessHist(action=completeness_plot), 

673 ) 

674 

675 def reconfigure_dependent_magnitudes( 

676 self, 

677 key_flux_meas: str | None = None, 

678 bands_color: dict[str, str] | list[str] | None = None, 

679 ): 

680 if key_flux_meas is not None: 

681 _set_field_config(self, name="mag_target", value=key_flux_meas) 

682 

683 def setDefaults(self): 

684 MagnitudeTool.setDefaults(self) 

685 MatchedRefCoaddTool.setDefaults(self) 

686 

687 self.mag_bins_plot.mag_interval = 100 

688 self.mag_bins_plot.mag_width = 200 

689 # Completeness/purity don't need a ref/target suffix as they are by 

690 # definition a function of ref/target mags, respectively 

691 self.name_suffix = "_mag{name_mag}" 

692 

693 

694class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffPlot): 

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

696 

697 Notes 

698 ----- 

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

700 to call on its own. 

701 """ 

702 

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

704 mag_y2 = Field[str]( 

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

706 default=None, 

707 optional=True, 

708 ) 

709 bands = DictField[str, str]( 

710 doc="Bands for colors. ", 

711 # The empty value for y is needed to indicate that it's a valid band 

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

713 ) 

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

715 

716 def _split_bands(self, band_list: str): 

717 # Split returns [""] for an empty string 

718 return band_list.split(self.band_separator) if band_list else [] 

719 

720 def finalize(self): 

721 # Check if it has already been finalized 

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

723 if self.mag_y2 is None: 

724 self.mag_y2 = self.mag_y1 

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

726 # This may result in duplicate actions but these are just plain 

727 # column selectors so that's not a serious problem 

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

729 n_bands = 0 

730 

731 # Set up mag actions for every band needed before finalizing plots 

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

733 for band2 in band2_list: 

734 mag_y1 = f"mag_y_{band1}" 

735 mag_y2 = f"mag_y_{band2}" 

736 mag_x1 = f"mag_x_{band1}" 

737 mag_x2 = f"mag_x_{band2}" 

738 self._set_flux_default(mag_y1, band=band1, name_mag=self.mag_y1) 

739 self._set_flux_default(mag_y2, band=band2, name_mag=self.mag_y2) 

740 self._set_flux_default(mag_x1, band=band1, name_mag=self.mag_x) 

741 self._set_flux_default(mag_x2, band=band2, name_mag=self.mag_x) 

742 n_bands += 1 

743 

744 # These two lines must appear in this order so that every color 

745 # has its plot actions finalized with a suffix (i.e., pointing 

746 # summary stats at yStars_0 instead of yStars). 

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

748 super().finalize() 

749 

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

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

752 

753 metric_base = self.produce.metric 

754 metric = metric_base 

755 plot_base = self.produce.plot 

756 

757 do_metrics = self.do_metrics() 

758 

759 actions_metric = {} 

760 actions_plot = {} 

761 

762 config_mag_x = self.config_mag_x 

763 config_mag_y = self.config_mag_y 

764 name_short_x = config_mag_x.name_flux_short 

765 name_short_y = config_mag_y.name_flux_short 

766 

767 idx = 0 

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

769 for band2 in band2_list: 

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

771 # Keep this index-based to simplify finalize 

772 suffix_y = f"_{idx}" 

773 self._set_actions(suffix=suffix_y) 

774 self.name_prefix = ( 

775 f"photom_{name_short_y}_vs_{name_short_x}_color_{name_color}" 

776 f"_{subtype}_{{name_type}}_" 

777 ) 

778 if do_metrics: 

779 metric = copy.copy(metric_base) 

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

781 

782 plot = copy.copy(plot_base) 

783 

784 plot.suffix_y = suffix_y 

785 plot.suffix_stat = suffix_y 

786 

787 mag_y1 = f"{self.mag_y1}_{band1}" 

788 mag_y2 = f"{self.mag_y2}_{band2}" 

789 mag_x1 = f"{self.mag_x}_{band1}" 

790 mag_x2 = f"{self.mag_x}_{band2}" 

791 

792 diff = ColorDiff( 

793 color1_flux1=getattr(self.process.buildActions, f"flux_{mag_y1}"), 

794 color1_flux2=getattr(self.process.buildActions, f"flux_{mag_y2}"), 

795 color2_flux1=getattr(self.process.buildActions, f"flux_{mag_x1}"), 

796 color2_flux2=getattr(self.process.buildActions, f"flux_{mag_x2}"), 

797 ) 

798 

799 if self.compute_chi: 

800 diff = DivideVector( 

801 actionA=diff, 

802 actionB=ColorError( 

803 flux_err1=DivideVector( 

804 actionA=getattr(self.process.buildActions, f"flux_err_{mag_y1}"), 

805 actionB=getattr(self.process.buildActions, f"flux_{mag_y1}"), 

806 ), 

807 flux_err2=DivideVector( 

808 actionA=getattr(self.process.buildActions, f"flux_err_{mag_y2}"), 

809 actionB=getattr(self.process.buildActions, f"flux_{mag_y2}"), 

810 ), 

811 ), 

812 ) 

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

814 

815 label = f"({band1} - {band2}) ({config_mag_y.name_flux} - {config_mag_x.name_flux})" 

816 label = f"χ = ({label})/σ" if self.compute_chi else f"{label} (mmag)" 

817 plot.yAxisLabel = label 

818 actions_metric[name_color] = metric 

819 actions_plot[name_color] = plot 

820 idx += 1 

821 if do_metrics: 

822 action_metric = StructMetricAction() 

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

824 setattr(action_metric.actions, name_action, action) 

825 self.produce.metric = action_metric 

826 action_plot = StructPlotAction() 

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

828 setattr(action_plot.actions, name_action, action) 

829 self.produce.plot = action_plot 

830 

831 def get_key_flux_y(self) -> str: 

832 return self.mag_y1 

833 

834 def reconfigure_dependent_magnitudes( 

835 self, 

836 key_flux_meas: str | None = None, 

837 bands_color: dict[str, str] | list[str] | None = None, 

838 ): 

839 if key_flux_meas is not None: 

840 _set_field_config(self, name="mag_y1", value=key_flux_meas) 

841 if bands_color is not None: 

842 if isinstance(bands_color, dict): 

843 _set_field_config(self, name="bands", value=bands_color) 

844 else: 

845 bands_new = {} 

846 bands_old = self.bands.default if inspect.isclass(self) else self.bands 

847 for band in bands_color: 

848 colors = bands_old.get(band) 

849 if colors is None: 

850 raise ValueError( 

851 f"Passed {bands_color=} to reconfigure colors for {self=} but {band=}" 

852 f" is not in {bands_old=}." 

853 ) 

854 bands_new[band] = ",".join(band for band in colors.split(",") if band in bands_color) 

855 _set_field_config(self, name="bands", value=bands_new) 

856 

857 def setDefaults(self): 

858 super().setDefaults() 

859 self.produce.plot.yLims = self.limits_diff_color_mmag_default 

860 

861 def validate(self): 

862 super().validate() 

863 errors = [] 

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

865 bands = self._split_bands(band2_list) 

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

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

868 if errors: 

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

870 

871 

872class MatchedRefCoaddDiffColorZoomTool(MatchedRefCoaddDiffColorTool): 

873 def setDefaults(self): 

874 super().setDefaults() 

875 self.produce.plot.yLims = self.limits_diff_color_mmag_zoom_default 

876 self.produce.metric = NoMetric 

877 

878 

879class MatchedRefCoaddChiColorTool(MatchedRefCoaddDiffColorTool): 

880 def setDefaults(self): 

881 super().setDefaults() 

882 self.compute_chi = True 

883 self.produce.plot.yLims = self.limits_chi_default 

884 

885 

886class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffPlot): 

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

888 

889 mag_y = pexConfig.Field[str]( 

890 default="cmodel_err", 

891 doc="Flux (magnitude) pexConfig.Field to difference against the x-axis values", 

892 ) 

893 measure_y_minus_x = pexConfig.Field[bool]( 

894 default=True, doc="Whether to plot the y-axis magnitude minus the x-axis; otherwise x-y if False." 

895 ) 

896 

897 def finalize(self): 

898 # Check if it has already been finalized 

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

900 # Ensure mag_y is set before any plot finalizes 

901 self._set_flux_default("mag_y") 

902 super().finalize() 

903 self._set_actions() 

904 name_short_x = self.config_mag_x.name_flux_short 

905 name_short_y = self.config_mag_y.name_flux_short 

906 

907 prefix_action = "flux" if self.compute_chi else "mag" 

908 actionA, actionB = ( 

909 getattr(self.process.buildActions, f"{prefix_action}_{mag}") 

910 for mag in ((self.mag_y, self.mag_x) if self.measure_y_minus_x else (self.mag_x, self.mag_y)) 

911 ) 

912 action_diff = SubtractVector(actionA=actionA, actionB=actionB) 

913 

914 if self.compute_chi: 

915 key_err = f"flux_err_{self.mag_y}" 

916 action_err = ( 

917 getattr(self.process.buildActions, key_err) 

918 if hasattr(self.process.buildActions, key_err) 

919 else getattr(self.process.buildActions, f"flux_err_{self.mag_x}") 

920 ) 

921 self.process.buildActions.diff = DivideVector(actionA=action_diff, actionB=action_err) 

922 else: 

923 # set to mmag 

924 self.process.buildActions.diff = MultiplyVector( 

925 actionA=action_diff, 

926 actionB=ConstantValue(value=1000.0), 

927 ) 

928 if not self.produce.plot.yAxisLabel: 

929 label_x, label_y = (mag.name_flux for mag in (self.config_mag_x, self.config_mag_y)) 

930 label = f"{label_y} - {label_x}" if self.measure_y_minus_x else f"{label_x} - {label_y}" 

931 self.produce.plot.yAxisLabel = f"χ = ({label})/σ" if self.compute_chi else f"{label} (mmag)" 

932 if self.unit is None: 

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

934 if self.name_prefix is None: 

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

936 self.name_prefix = f"photom_{name_short_y}_vs_{name_short_x}_mag_{subtype}_{{name_type}}_" 

937 if self.do_metrics() and not self.produce.metric.units: 

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

939 

940 def get_key_flux_y(self) -> str: 

941 return self.mag_y 

942 

943 def reconfigure_dependent_magnitudes( 

944 self, 

945 key_flux_meas: str | None = None, 

946 bands_color: dict[str, str] | None = None, 

947 ): 

948 if key_flux_meas is not None: 

949 _set_field_config(self, name="mag_y", value=key_flux_meas) 

950 

951 def setDefaults(self): 

952 super().setDefaults() 

953 self.produce.plot.yLims = self.limits_diff_mag_mmag_default 

954 

955 

956class MatchedRefCoaddDiffMagZoomTool(MatchedRefCoaddDiffMagTool): 

957 def setDefaults(self): 

958 super().setDefaults() 

959 self.produce.plot.yLims = self.limits_diff_mag_mmag_zoom_default 

960 self.produce.metric = NoMetric 

961 

962 

963class MatchedRefCoaddChiMagTool(MatchedRefCoaddDiffMagTool): 

964 def setDefaults(self): 

965 super().setDefaults() 

966 self.compute_chi = True 

967 self.produce.plot.yLims = self.limits_chi_default 

968 

969 

970class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffPlot): 

971 """Tool for diffs between reference and measured coadd positions.""" 

972 

973 coord_label = Field[str]( 

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

975 optional=True, 

976 default=None, 

977 ) 

978 coord_meas = Field[str]( 

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

980 optional=False, 

981 ) 

982 coord_ref = Field[str]( 

983 doc="The key for reference values of the astrometric variable", 

984 optional=False, 

985 ) 

986 coord_ref_cos = Field[str]( 

987 doc="The key for reference values of the cosine correction astrometric variable" 

988 " (i.e. dec if coord_meas is RA)", 

989 default=None, 

990 optional=True, 

991 ) 

992 coord_ref_cos_unit = Field[str]( 

993 doc="astropy unit of coord_ref_cos", 

994 default="deg", 

995 optional=True, 

996 ) 

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

998 # Default coords are in degrees and we want mas 

999 scale_factor = Field[float]( 

1000 doc="The factor to multiply distances by (e.g. the pixel scale if coordinates have pixel units or " 

1001 "the desired spherical coordinate unit conversion otherwise)", 

1002 default=3600000, 

1003 ) 

1004 

1005 def finalize(self): 

1006 # Check if it has already been finalized 

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

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

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

1010 self._set_flux_default("mag_sn") 

1011 super().finalize() 

1012 self._set_actions() 

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

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

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

1016 name_short_x = self.config_mag_x.name_flux_short 

1017 name_short_y = self.config_mag_y.name_flux_short 

1018 

1019 if self.compute_chi: 

1020 self.process.buildActions.diff = DivideVector( 

1021 actionA=SubtractVector( 

1022 actionA=self.process.buildActions.pos_meas, 

1023 actionB=self.process.buildActions.pos_ref, 

1024 ), 

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

1026 ) 

1027 else: 

1028 factor = ConstantValue(value=self.scale_factor) 

1029 if self.coord_ref_cos: 

1030 factor_cos = u.Unit(self.coord_ref_cos_unit).to(u.rad) 

1031 factor = MultiplyVector( 

1032 actionA=factor, 

1033 actionB=CosVector( 

1034 actionA=MultiplyVector( 

1035 actionA=ConstantValue(value=factor_cos), 

1036 actionB=LoadVector(vectorKey=self.coord_ref_cos), 

1037 ) 

1038 ), 

1039 ) 

1040 self.process.buildActions.diff = MultiplyVector( 

1041 actionA=factor, 

1042 actionB=SubtractVector( 

1043 actionA=self.process.buildActions.pos_meas, 

1044 actionB=self.process.buildActions.pos_ref, 

1045 ), 

1046 ) 

1047 if self.unit is None: 

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

1049 if self.name_prefix is None: 

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

1051 coord_prefix = "" if "coord" in self.coord_meas else "coord_" 

1052 self.name_prefix = ( 

1053 f"astrom_{name_short_y}_vs_{name_short_x}_{coord_prefix}{self.coord_meas}_{subtype}" 

1054 f"_{{name_type}}_" 

1055 ) 

1056 if self.do_metrics() and not self.produce.metric.units: 

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

1058 if not self.produce.plot.yAxisLabel: 

1059 label = f"({name_short_y} - {name_short_x})" 

1060 coord_suffix = "" if "coord" in name else " coord" 

1061 self.produce.plot.yAxisLabel = ( 

1062 f"χ = ({label} {name}{coord_suffix})/σ" 

1063 if self.compute_chi 

1064 else f"{label} {name}{coord_suffix} ({self.unit})" 

1065 ) 

1066 

1067 def get_key_flux_y(self) -> str: 

1068 return self.mag_sn 

1069 

1070 def reconfigure_dependent_magnitudes( 

1071 self, 

1072 key_flux_meas: str | None = None, 

1073 bands_color: dict[str, str] | None = None, 

1074 ): 

1075 if key_flux_meas is not None: 

1076 _set_field_config(self, name="mag_sn", value=key_flux_meas) 

1077 

1078 def setDefaults(self): 

1079 super().setDefaults() 

1080 self.produce.plot.yLims = self.limits_diff_pos_mas_default 

1081 

1082 

1083class MatchedRefCoaddDiffPositionZoomTool(MatchedRefCoaddDiffPositionTool): 

1084 def setDefaults(self): 

1085 super().setDefaults() 

1086 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1087 self.produce.metric = NoMetric 

1088 

1089 

1090class MatchedRefCoaddDiffCoordRaTool(MatchedRefCoaddDiffPositionTool): 

1091 def setDefaults(self): 

1092 super().setDefaults() 

1093 self.coord_meas = "coord_ra" 

1094 self.coord_ref = "ref_ra" 

1095 self.coord_ref_cos = "ref_dec" 

1096 

1097 

1098class MatchedRefCoaddDiffCoordRaZoomTool(MatchedRefCoaddDiffCoordRaTool): 

1099 def setDefaults(self): 

1100 super().setDefaults() 

1101 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1102 self.produce.metric = NoMetric 

1103 

1104 

1105class MatchedRefCoaddChiCoordRaTool(MatchedRefCoaddDiffCoordRaTool): 

1106 def setDefaults(self): 

1107 super().setDefaults() 

1108 self.compute_chi = True 

1109 self.produce.plot.yLims = self.limits_chi_default 

1110 

1111 

1112class MatchedRefCoaddDiffCoordDecTool(MatchedRefCoaddDiffPositionTool): 

1113 def setDefaults(self): 

1114 super().setDefaults() 

1115 self.coord_meas = "coord_dec" 

1116 self.coord_ref = "ref_dec" 

1117 

1118 

1119class MatchedRefCoaddDiffCoordDecZoomTool(MatchedRefCoaddDiffCoordDecTool): 

1120 def setDefaults(self): 

1121 super().setDefaults() 

1122 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1123 self.produce.metric = NoMetric 

1124 

1125 

1126class MatchedRefCoaddChiCoordDecTool(MatchedRefCoaddDiffCoordDecTool): 

1127 def setDefaults(self): 

1128 super().setDefaults() 

1129 self.compute_chi = True 

1130 self.produce.plot.yLims = self.limits_chi_default 

1131 

1132 

1133class MatchedRefCoaddDiffDistanceTool(MatchedRefCoaddDiffPlot): 

1134 """Tool for distances between matched reference and measured coadd 

1135 objects.""" 

1136 

1137 key_dist = Field[str](default="match_distance", doc="Distance field key") 

1138 key_dist_err = Field[str](default="match_distanceErr", doc="Distance error field key") 

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

1140 # Default coords are in degrees and we want mas 

1141 scale_factor = Field[float]( 

1142 doc="The factor to multiply distances by (e.g. the pixel scale if coordinates have pixel units or " 

1143 "the desired spherical coordinate unit conversion otherwise)", 

1144 default=3600000, 

1145 ) 

1146 

1147 def finalize(self): 

1148 # Check if it has already been finalized 

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

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

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

1152 self._set_flux_default("mag_sn") 

1153 super().finalize() 

1154 self._set_actions() 

1155 

1156 name_short_x = self.config_mag_x.name_flux_short 

1157 name_short_y = self.config_mag_y.name_flux_short 

1158 

1159 self.process.buildActions.dist = LoadVector(vectorKey=self.key_dist) 

1160 if self.compute_chi: 

1161 self.process.buildActions.diff = DivideVector( 

1162 actionA=self.process.buildActions.dist, 

1163 actionB=LoadVector(vectorKey=self.key_dist_err), 

1164 ) 

1165 else: 

1166 self.process.buildActions.diff = MultiplyVector( 

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

1168 actionB=self.process.buildActions.dist, 

1169 ) 

1170 if self.unit is None: 

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

1172 if self.name_prefix is None: 

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

1174 self.name_prefix = f"astrom_dist_{{name_type}}_{subtype}_" 

1175 self.name_prefix = f"astrom_{name_short_y}_vs_{name_short_x}_dist_{subtype}_{{name_type}}_" 

1176 if self.do_metrics() and not self.produce.metric.units: 

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

1178 if not self.produce.plot.yAxisLabel: 

1179 label = f"({name_short_y} - {name_short_x}) distance" 

1180 self.produce.plot.yAxisLabel = ( 

1181 f"χ = ({label})/σ" if self.compute_chi else f"{label} ({self.unit})" 

1182 ) 

1183 

1184 def get_key_flux_y(self) -> str: 

1185 return self.mag_sn 

1186 

1187 def reconfigure_dependent_magnitudes( 

1188 self, 

1189 key_flux_meas: str | None = None, 

1190 bands_color: dict[str, str] | None = None, 

1191 ): 

1192 if key_flux_meas is not None: 

1193 _set_field_config(self, name="mag_sn", value=key_flux_meas) 

1194 

1195 def setDefaults(self): 

1196 super().setDefaults() 

1197 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_default[1]] 

1198 

1199 

1200class MatchedRefCoaddChiDistanceTool(MatchedRefCoaddDiffDistanceTool): 

1201 def setDefaults(self): 

1202 super().setDefaults() 

1203 self.compute_chi = True 

1204 self.produce.plot.yLims = self.limits_chi_default 

1205 

1206 

1207class MatchedRefCoaddDiffDistanceZoomTool(MatchedRefCoaddDiffDistanceTool): 

1208 def setDefaults(self): 

1209 super().setDefaults() 

1210 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_zoom_default[1]] 

1211 self.produce.metric = NoMetric 

1212 

1213 

1214def reconfigure_diff_matched_defaults( 

1215 config: AnalysisBaseConfig | None = None, 

1216 context: str | None = None, 

1217 key_flux_meas: str | None = None, 

1218 bands_color: dict[str, str] | list[str] | None = None, 

1219 use_any: bool | None = None, 

1220 use_galaxies: bool | None = None, 

1221 use_stars: bool | None = None, 

1222): 

1223 """Reconfigure the default values for config fields of MatchedRefCoaddTool 

1224 and all of its subclasses. 

1225 

1226 Parameters 

1227 ---------- 

1228 config 

1229 An existing analysis config. Overrides will be applied to any of its 

1230 member MatchedRefCoaddTool atools. 

1231 context 

1232 The context to set. Must be a valid choice for 

1233 MatchedRefCoaddTool.context. 

1234 key_flux_meas 

1235 The key of the measured flux config to use, e.g. "psf". If the key is 

1236 not found, it will search for f"{key}_err", the default name for 

1237 configurations that load error keys as well as fluxes. 

1238 bands_color 

1239 A dictionary keyed by band of comma-separated bands to measure 

1240 colors for, where the color is (key - value). If a list is passed, 

1241 tools will modify the defaults to select only those bands within 

1242 the list (which should also be a set). 

1243 use_any 

1244 Whether to compute metrics for objects of all types. 

1245 use_galaxies 

1246 Whether to compute metrics and plot lines for galaxies only. 

1247 use_stars 

1248 Whether to compute metrics and plot lines for stars only. 

1249 

1250 Notes 

1251 ----- 

1252 Any kwargs set to None will not change the relevant config field defaults. 

1253 """ 

1254 if key_flux_meas is not None: 

1255 keys_flux = tuple(MagnitudeTool.fluxes_default.toDict().keys()) 

1256 if key_flux_meas not in keys_flux: 

1257 key_flux_err = f"{key_flux_meas}_err" 

1258 if key_flux_err not in keys_flux: 

1259 raise ValueError( 

1260 f"{key_flux_meas=} and {key_flux_err} not found in available keys: {keys_flux}" 

1261 f" (from MagnitudeTool.fluxes_default.toDict().keys())" 

1262 ) 

1263 key_flux_meas = key_flux_err 

1264 

1265 # These are class attributes and don't need to be changed in subclasses 

1266 # These may end up being changed multiple times with repeated calls, 

1267 # but there isn't a good way to avoid that. 

1268 MagnitudeTool.fluxes_default.ref_matched.name_flux = "True" 

1269 MagnitudeTool.fluxes_default.ref_matched.name_flux_short = "true" 

1270 MagnitudeTool.fluxes_default.ref_matched.key_flux = "ref_{band}_flux" 

1271 

1272 def all_subclasses(cls): 

1273 return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)]) 

1274 

1275 subclasses = all_subclasses(MatchedRefCoaddTool) 

1276 

1277 """ 

1278 Further context on these kwargs (get it?) and why they're not contexts: 

1279 

1280 context (DC2, source_injection, etc) 

1281 This could be made an AnalysisContext, but ChoiceField has the 

1282 benefits of automatic validation. Also, subclasses refer to this config 

1283 field without having to implement separate context functions. 

1284 key_flux_meas 

1285 This is the key to a FluxConfig. Default FluxConfigs could be mapped 

1286 onto an AnalysisContext instead. 

1287 bands_color 

1288 This applies only to color tools and is intended to be set by 

1289 obs package config overrides, e.g. to drop u-band colours. There is no 

1290 way for obs packages to change Tool instance values and the bands 

1291 config field is part of the PipelineTask and not accessible to tools, 

1292 so no obvious alternative exists. 

1293 use_* 

1294 Like context, these apply to all subclasses, but are independent 

1295 booleans rather than exclusive choices. 

1296 """ 

1297 

1298 # This sets defaults for all known subclasses 

1299 for tool in subclasses: 

1300 tool.reconfigure( 

1301 tool, 

1302 context=context, 

1303 key_flux_meas=key_flux_meas, 

1304 bands_color=bands_color, 

1305 use_any=use_any, 

1306 use_galaxies=use_galaxies, 

1307 use_stars=use_stars, 

1308 ) 

1309 

1310 # This sets defaults for all existing tools 

1311 # If a pipeline A imports a pipeline B, any atools already set in B will 

1312 # be instantiated before overrides from A are applied. Therefore, changing 

1313 # only the defaults will have no effect on those existing tools. 

1314 if config is not None: 

1315 for tool in config.atools: 

1316 if isinstance(tool, MatchedRefCoaddTool): 

1317 tool: MatchedRefCoaddTool = tool 

1318 tool.reconfigure( 

1319 context=context, 

1320 key_flux_meas=key_flux_meas, 

1321 bands_color=bands_color, 

1322 use_any=use_any, 

1323 use_galaxies=use_galaxies, 

1324 use_stars=use_stars, 

1325 )