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

543 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 09:21 +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 

51 

52import lsst.pex.config as pexConfig 

53from lsst.pex.config import DictField, Field 

54from lsst.pex.config.configurableActions import ConfigurableActionField 

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

56 

57from ..actions.config import MagnitudeBinConfig 

58from ..actions.keyedData import ( 

59 CalcBinnedCompletenessAction, 

60 CalcCompletenessHistogramAction, 

61 MagnitudeCompletenessConfig, 

62) 

63from ..actions.plot import CompletenessHist 

64from ..actions.vector import ( 

65 CalcBinnedStatsAction, 

66 ColorDiff, 

67 ColorError, 

68 ConstantValue, 

69 CosVector, 

70 DivideVector, 

71 DownselectVector, 

72 IsMatchedObjectSameClass, 

73 LoadVector, 

74 MultiplyVector, 

75 SubtractVector, 

76) 

77from ..actions.vector.selectors import ( 

78 InjectedGalaxySelector, 

79 InjectedObjectSelector, 

80 InjectedStarSelector, 

81 MatchedObjectSelector, 

82 RangeSelector, 

83 ReferenceGalaxySelector, 

84 ReferenceObjectSelector, 

85 ReferenceStarSelector, 

86 SelectorBase, 

87 VectorSelector, 

88) 

89from ..interfaces import AnalysisBaseConfig, BaseMetricAction, NoMetric 

90from .genericBuild import MagnitudeTool, MagnitudeXTool, ObjectClassTool 

91from .genericMetricAction import StructMetricAction 

92from .genericPlotAction import StructPlotAction 

93from .genericProduce import MagnitudeScatterPlot 

94 

95 

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

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

98 

99 Parameters 

100 ---------- 

101 config 

102 A Config instance or metaclass. 

103 name 

104 The name of the attribute to set. 

105 value 

106 The value to set it to. 

107 """ 

108 if isinstance(config, pexConfig.ConfigMeta): 

109 getattr(config, name).default = value 

110 else: 

111 setattr(config, name, value) 

112 

113 

114class MatchedRefCoaddTool(ObjectClassTool): 

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

116 

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

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

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

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

121 

122 Notes 

123 ----- 

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

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

126 appropriate candidates (and stores a match_candidate column). 

127 

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

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

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

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

132 there are no reference objects of the given class. 

133 """ 

134 

135 _suffix_ref = "_ref" 

136 _suffix_target = "_target" 

137 

138 context = pexConfig.ChoiceField[str]( 

139 doc="The context for the selectors", 

140 allowed={ 

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

142 "DC2": "DC2 Truth Summary match", 

143 "injection": "Source injection match", 

144 }, 

145 default="DC2", 

146 ) 

147 

148 select_ref_by_default = pexConfig.Field[bool]( 

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

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

151 default=True, 

152 ) 

153 

154 selector_ref_all = ConfigurableActionField[SelectorBase]( 

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

156 default=ReferenceObjectSelector, 

157 ) 

158 selector_ref_galaxy = ConfigurableActionField[SelectorBase]( 

159 doc="The selector for reference galaxies", 

160 default=ReferenceGalaxySelector, 

161 ) 

162 selector_ref_star = ConfigurableActionField[SelectorBase]( 

163 doc="The selector for reference stars", 

164 default=ReferenceStarSelector, 

165 ) 

166 

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

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

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

170 name_prefix = pexConfig.Field[str]( 

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

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

173 default=None, 

174 optional=True, 

175 ) 

176 name_suffix = pexConfig.Field[str]( 

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

178 " template for the magnitude algorithm", 

179 default="_ref_mag{name_mag}", 

180 ) 

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

182 

183 def finalize(self): 

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

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

186 # be kept in sync manually 

187 if self.context != "DC2": 

188 match self.context: 

189 case "injection": 

190 self.selector_ref_all = InjectedObjectSelector() 

191 self.selector_ref_galaxy = InjectedGalaxySelector() 

192 self.selector_ref_star = InjectedStarSelector() 

193 case "custom": 

194 pass 

195 case _: 

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

197 

198 # Other tools will except selector_all 

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

200 

201 super().finalize() 

202 

203 for object_class in self.get_classes(): 

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

205 selector = self.get_selector_ref(object_class) 

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

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

208 # several parallel selections 

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

210 

211 def get_selector_ref(self, object_class: str): 

212 match object_class: 

213 case "any": 

214 return self.selector_ref_all 

215 case "galaxy": 

216 return self.selector_ref_galaxy 

217 case "star": 

218 return self.selector_ref_star 

219 

220 def reconfigure( 

221 self, 

222 context: str | None = None, 

223 key_flux_meas: str | None = None, 

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

225 use_any: bool | None = None, 

226 use_galaxies: bool | None = None, 

227 use_stars: bool | None = None, 

228 ): 

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

230 

231 Parameters 

232 ---------- 

233 context 

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

235 MatchedRefCoaddTool.context. 

236 key_flux_meas 

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

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

239 configurations that load error keys as well as fluxes. 

240 bands_color 

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

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

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

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

245 use_any 

246 Whether to compute metrics for objects of all types. 

247 use_galaxies 

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

249 use_stars 

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

251 

252 Notes 

253 ----- 

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

255 """ 

256 if context is not None: 

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

258 if use_any is not None: 

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

260 if use_galaxies is not None: 

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

262 if use_stars is not None: 

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

264 

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

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

267 

268 # Change any dependent magnitudes 

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

270 

271 def reconfigure_dependent_magnitudes( 

272 self, 

273 key_flux_meas: str | None = None, 

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

275 ): 

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

277 column configs. 

278 

279 Parameters 

280 ---------- 

281 key_flux_meas 

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

283 bands_color 

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

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

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

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

288 """ 

289 

290 def setDefaults(self): 

291 super().setDefaults() 

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

293 self.selector_ref_galaxy.plotLabelKey = None 

294 self.selector_ref_star.plotLabelKey = None 

295 

296 

297class MatchedRefCoaddDiffTool(MagnitudeXTool, MatchedRefCoaddTool): 

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

299 

300 limits_chi_default = (-5, 5) 

301 limits_diff_color_mmag_default = (-250.0, 250.0) 

302 limits_diff_color_mmag_zoom_default = (-50.0, 50.0) 

303 limits_diff_mag_mmag_default = (-1000.0, 1000.0) 

304 limits_diff_mag_mmag_zoom_default = (-50.0, 50.0) 

305 limits_diff_pos_mas_default = (-500, 500) 

306 limits_diff_pos_mas_zoom_default = (-10, 10) 

307 limits_x_mag_default = (16.5, 29.0) 

308 limits_x_mag_zoom_default = (16.5, 24.0) 

309 

310 compute_chi = pexConfig.Field[bool]( 

311 default=False, 

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

313 ) 

314 

315 def _set_actions(self, suffix=None): 

316 if suffix is None: 

317 suffix = "" 

318 

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

320 for object_class in self.get_classes(): 

321 name_type_plural = self.get_class_name_plural(object_class) 

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

323 name_selector = self.get_name_attr_selector(object_class, selection) 

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

325 

326 y_values = DownselectVector( 

327 vectorKey=f"diff{suffix}", 

328 selector=VectorSelector(vectorKey=name_selector), 

329 ) 

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

331 

332 bins = self.mag_bins.get_bins() 

333 for minimum in bins: 

334 setattr( 

335 self.process.calculateActions, 

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

337 CalcBinnedStatsAction( 

338 key_vector=name_attr, 

339 selector_range=RangeSelector( 

340 vectorKey=name_x, 

341 minimum=minimum, 

342 maximum=minimum + self.mag_bins.mag_width, 

343 ), 

344 ), 

345 ) 

346 

347 def configureMetrics( 

348 self, 

349 unit: str | None = None, 

350 name_prefix: str | None = None, 

351 attr_suffix: str | None = None, 

352 unit_select: str = "mag", 

353 ): 

354 """Configure metric actions and return units. 

355 

356 Parameters 

357 ---------- 

358 unit : `str` 

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

360 name_prefix : `str` 

361 The prefix for the action (column) name. 

362 attr_suffix : `str` 

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

364 unit_select : `str` 

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

366 

367 Returns 

368 ------- 

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

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

371 """ 

372 if unit is None: 

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

374 if name_prefix is None: 

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

376 if attr_suffix is None: 

377 attr_suffix = "" 

378 

379 if unit_select is None: 

380 unit_select = "mag" 

381 

382 key_flux = self.config_mag_x.key_flux 

383 

384 units = {} 

385 

386 for object_class in self.get_classes(): 

387 name_type = self.get_class_type(object_class) 

388 name_type_plural = self.get_class_name_plural(object_class) 

389 name_capital = name_type_plural.capitalize() 

390 x_key = f"x{name_capital}" 

391 

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

393 bins = self.mag_bins.get_bins() 

394 for minimum in bins: 

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

396 action.selector_range = RangeSelector( 

397 vectorKey=x_key, 

398 minimum=minimum / 1000.0, 

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

400 ) 

401 name_mag = self.mag_bins.get_name_bin(minimum) 

402 

403 action.name_prefix = name_prefix.format( 

404 key_flux=key_flux, 

405 name_type=name_type, 

406 ) 

407 if self.parameterizedBand: 

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

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

410 

411 units.update( 

412 { 

413 action.name_median: unit, 

414 action.name_sigmaMad: unit, 

415 action.name_count: "count", 

416 action.name_select_median: unit_select, 

417 } 

418 ) 

419 return units 

420 

421 @property 

422 def config_mag_y(self): 

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

424 

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

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

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

428 mag_y = self.get_key_flux_y() 

429 if mag_y not in self.fluxes: 

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

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

432 assert mag_y in self.fluxes 

433 return self.fluxes[mag_y] 

434 

435 def finalize(self): 

436 MagnitudeXTool.finalize(self) 

437 MatchedRefCoaddTool.finalize(self) 

438 

439 @abstractmethod 

440 def get_key_flux_y(self) -> str: 

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

442 raise NotImplementedError("subclasses must implement get_key_flux_y") 

443 

444 def setDefaults(self): 

445 MagnitudeXTool.setDefaults(self) 

446 MatchedRefCoaddTool.setDefaults(self) 

447 self.mag_x = "ref_matched" 

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

449 

450 

451class MatchedRefCoaddDiffPlot(MatchedRefCoaddDiffTool, MagnitudeScatterPlot): 

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

453 with a scatter plot.""" 

454 

455 def do_metrics(self): 

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

457 

458 def get_key_flux_y(self) -> str: 

459 return super().get_key_flux_y() 

460 

461 def finalize(self): 

462 MatchedRefCoaddDiffTool.finalize(self) 

463 MagnitudeScatterPlot.finalize(self) 

464 

465 def setDefaults(self): 

466 # This will set no plot 

467 MatchedRefCoaddDiffTool.setDefaults(self) 

468 # This will set the plot 

469 MagnitudeScatterPlot.setDefaults(self) 

470 self.produce.plot.xLims = self.limits_x_mag_default 

471 

472 

473class MatchedRefCoaddCompurityTool(MagnitudeTool, MatchedRefCoaddTool): 

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

475 

476 By contrast with MatchedRefCoaddDiffTool, where one must choose which 

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

478 different magnitudes. The completeness plot necessarily is a function 

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

480 magnitude. 

481 """ 

482 

483 config_metrics = pexConfig.ConfigField[MagnitudeCompletenessConfig]( 

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

485 ) 

486 key_match_distance = pexConfig.Field[str]( 

487 default="match_distance", 

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

489 ) 

490 mag_bins_plot = pexConfig.ConfigField[MagnitudeBinConfig]( 

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

492 "(including completeness at magnitude thresholds)" 

493 ) 

494 mag_ref = pexConfig.Field[str]( 

495 default="ref_matched", 

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

497 ) 

498 mag_target = pexConfig.Field[str]( 

499 default="cmodel_err", 

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

501 ) 

502 make_plots = pexConfig.Field[bool]( 

503 default=True, 

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

505 ) 

506 

507 @property 

508 def config_mag_ref(self): 

509 return self._config_mag("mag_ref") 

510 

511 @property 

512 def config_mag_target(self): 

513 return self._config_mag("mag_target") 

514 

515 def finalize(self): 

516 if not self.produce.metric.units: 

517 MagnitudeTool.finalize(self) 

518 MatchedRefCoaddTool.finalize(self) 

519 self._set_flux_default("mag_ref") 

520 self._set_flux_default("mag_target") 

521 

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

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

524 # (including injection catalogs) 

525 name_prefix = ( 

526 self.name_prefix 

527 if self.name_prefix 

528 else ( 

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

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

531 ) 

532 ) 

533 unit_select = "" 

534 kwargs_matched_class_action = {} 

535 

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

537 # the wrong/right matched class selector 

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

539 for suffix, func_selector in ( 

540 (self._suffix_ref, self.get_selector_ref), 

541 (self._suffix_target, self.get_selector), 

542 ): 

543 name_selector = self.get_name_attr_selector(object_class, suffix) 

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

545 selector = func_selector(object_class) 

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

547 if object_class != "any": 

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

549 

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

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

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

553 

554 key_flux = self.config_mag_ref.key_flux 

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

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

557 object_classes = self.get_classes() 

558 self.produce.metric = StructMetricAction() 

559 if self.make_plots: 

560 self.produce.plot = StructPlotAction() 

561 

562 for object_class in object_classes: 

563 name_type = self.get_class_type(object_class) 

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

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

566 name_prefix_class = name_prefix.format( 

567 key_flux=key_flux, 

568 name_type=name_type, 

569 ) 

570 if self.parameterizedBand: 

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

572 

573 units = {} 

574 completeness_binned_metrics = CalcCompletenessHistogramAction( 

575 action=CalcBinnedCompletenessAction( 

576 name_prefix=name_prefix_class, 

577 selector_range_ref=RangeSelector(vectorKey=key_mag_ref), 

578 selector_range_target=RangeSelector(vectorKey=key_mag_target), 

579 key_mask_ref=name_selector_ref, 

580 key_mask_target=name_selector_target, 

581 ), 

582 bins=self.mag_bins, 

583 ) 

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

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

586 # given completeness/purity) 

587 completeness_binned_metrics.config_metrics.completeness_percentiles = [] 

588 setattr( 

589 self.process.calculateActions, 

590 f"completeness_binned_metrics_{object_class}", 

591 completeness_binned_metrics, 

592 ) 

593 

594 bins = self.mag_bins.get_bins() 

595 for minimum in bins: 

596 name_mag = self.mag_bins.get_name_bin(minimum) 

597 action = CalcBinnedCompletenessAction( 

598 name_prefix=name_prefix_class, 

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

600 selector_range_ref=RangeSelector( 

601 vectorKey=key_mag_ref, 

602 minimum=minimum / 1000.0, 

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

604 ), 

605 selector_range_target=RangeSelector( 

606 vectorKey=key_mag_target, 

607 minimum=minimum / 1000.0, 

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

609 ), 

610 key_mask_ref=name_selector_ref, 

611 key_mask_target=name_selector_target, 

612 ) 

613 setattr( 

614 self.process.calculateActions, 

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

616 action, 

617 ) 

618 

619 units.update( 

620 { 

621 action.name_count: "count", 

622 action.name_count_ref: "count", 

623 action.name_count_target: "count", 

624 action.name_completeness: unit_select, 

625 action.name_completeness_bad_match: unit_select, 

626 action.name_completeness_good_match: unit_select, 

627 action.name_purity: unit_select, 

628 action.name_purity_bad_match: unit_select, 

629 action.name_purity_good_match: unit_select, 

630 } 

631 ) 

632 

633 completeness_plot = CalcCompletenessHistogramAction( 

634 action=CalcBinnedCompletenessAction( 

635 name_prefix=name_prefix_class, 

636 selector_range_ref=RangeSelector(vectorKey=key_mag_ref), 

637 selector_range_target=RangeSelector(vectorKey=key_mag_target), 

638 key_mask_ref=name_selector_ref, 

639 key_mask_target=name_selector_target, 

640 ), 

641 bins=self.mag_bins_plot, 

642 config_metrics=self.config_metrics, 

643 ) 

644 setattr( 

645 self.process.calculateActions, 

646 f"completeness_plot_{object_class}", 

647 completeness_plot, 

648 ) 

649 for pct in completeness_plot.config_metrics.completeness_percentiles: 

650 name_pct = completeness_plot.action.name_mag_completeness( 

651 completeness_plot.getPercentileName(pct) 

652 ) 

653 units[name_pct] = unit_select 

654 

655 # Make the metric action for the given object class 

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

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

658 setattr( 

659 self.produce.metric.actions, 

660 object_class, 

661 BaseMetricAction(units=units), 

662 ) 

663 

664 if self.make_plots: 

665 overrides = {} 

666 if name_type == self.type_galaxies: 

667 overrides["color_counts"] = galaxies_color() 

668 elif name_type == self.type_stars: 

669 overrides["color_counts"] = stars_color() 

670 setattr( 

671 self.produce.plot.actions, 

672 object_class, 

673 CompletenessHist(action=completeness_plot), 

674 ) 

675 

676 def reconfigure_dependent_magnitudes( 

677 self, 

678 key_flux_meas: str | None = None, 

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

680 ): 

681 if key_flux_meas is not None: 

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

683 

684 def setDefaults(self): 

685 MagnitudeTool.setDefaults(self) 

686 MatchedRefCoaddTool.setDefaults(self) 

687 

688 self.mag_bins_plot.mag_interval = 100 

689 self.mag_bins_plot.mag_width = 200 

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

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

692 self.name_suffix = "_mag{name_mag}" 

693 

694 

695class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffPlot): 

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

697 

698 Notes 

699 ----- 

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

701 to call on its own. 

702 """ 

703 

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

705 mag_y2 = Field[str]( 

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

707 default=None, 

708 optional=True, 

709 ) 

710 bands = DictField[str, str]( 

711 doc="Bands for colors. ", 

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

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

714 ) 

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

716 

717 def _split_bands(self, band_list: str): 

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

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

720 

721 def finalize(self): 

722 # Check if it has already been finalized 

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

724 if self.mag_y2 is None: 

725 self.mag_y2 = self.mag_y1 

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

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

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

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

730 n_bands = 0 

731 

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

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

734 for band2 in band2_list: 

735 mag_y1 = f"mag_y_{band1}" 

736 mag_y2 = f"mag_y_{band2}" 

737 mag_x1 = f"mag_x_{band1}" 

738 mag_x2 = f"mag_x_{band2}" 

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

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

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

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

743 n_bands += 1 

744 

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

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

747 # summary stats at yStars_0 instead of yStars). 

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

749 super().finalize() 

750 

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

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

753 

754 metric_base = self.produce.metric 

755 metric = metric_base 

756 plot_base = self.produce.plot 

757 

758 do_metrics = self.do_metrics() 

759 

760 actions_metric = {} 

761 actions_plot = {} 

762 

763 config_mag_x = self.config_mag_x 

764 config_mag_y = self.config_mag_y 

765 name_short_x = config_mag_x.name_flux_short 

766 name_short_y = config_mag_y.name_flux_short 

767 

768 idx = 0 

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

770 for band2 in band2_list: 

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

772 # Keep this index-based to simplify finalize 

773 suffix_y = f"_{idx}" 

774 self._set_actions(suffix=suffix_y) 

775 self.name_prefix = ( 

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

777 f"_{subtype}_{{name_type}}_" 

778 ) 

779 if do_metrics: 

780 metric = copy.copy(metric_base) 

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

782 

783 plot = copy.copy(plot_base) 

784 

785 plot.suffix_y = suffix_y 

786 plot.suffix_stat = suffix_y 

787 

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

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

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

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

792 

793 diff = ColorDiff( 

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

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

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

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

798 ) 

799 

800 if self.compute_chi: 

801 diff = DivideVector( 

802 actionA=diff, 

803 actionB=ColorError( 

804 flux_err1=DivideVector( 

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

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

807 ), 

808 flux_err2=DivideVector( 

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

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

811 ), 

812 ), 

813 ) 

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

815 

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

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

818 plot.yAxisLabel = label 

819 actions_metric[name_color] = metric 

820 actions_plot[name_color] = plot 

821 idx += 1 

822 if do_metrics: 

823 action_metric = StructMetricAction() 

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

825 setattr(action_metric.actions, name_action, action) 

826 self.produce.metric = action_metric 

827 action_plot = StructPlotAction() 

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

829 setattr(action_plot.actions, name_action, action) 

830 self.produce.plot = action_plot 

831 

832 def get_key_flux_y(self) -> str: 

833 return self.mag_y1 

834 

835 def reconfigure_dependent_magnitudes( 

836 self, 

837 key_flux_meas: str | None = None, 

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

839 ): 

840 if key_flux_meas is not None: 

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

842 if bands_color is not None: 

843 if isinstance(bands_color, dict): 

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

845 else: 

846 bands_new = {} 

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

848 for band in bands_color: 

849 colors = bands_old.get(band) 

850 if colors is None: 

851 raise ValueError( 

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

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

854 ) 

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

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

857 

858 def setDefaults(self): 

859 super().setDefaults() 

860 self.produce.plot.yLims = self.limits_diff_color_mmag_default 

861 

862 def validate(self): 

863 super().validate() 

864 errors = [] 

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

866 bands = self._split_bands(band2_list) 

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

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

869 if errors: 

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

871 

872 

873class MatchedRefCoaddDiffColorZoomTool(MatchedRefCoaddDiffColorTool): 

874 def setDefaults(self): 

875 super().setDefaults() 

876 self.produce.plot.yLims = self.limits_diff_color_mmag_zoom_default 

877 self.produce.metric = NoMetric 

878 

879 

880class MatchedRefCoaddChiColorTool(MatchedRefCoaddDiffColorTool): 

881 def setDefaults(self): 

882 super().setDefaults() 

883 self.compute_chi = True 

884 self.produce.plot.yLims = self.limits_chi_default 

885 

886 

887class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffPlot): 

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

889 

890 mag_y = pexConfig.Field[str]( 

891 default="cmodel_err", 

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

893 ) 

894 measure_y_minus_x = pexConfig.Field[bool]( 

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

896 ) 

897 

898 def finalize(self): 

899 # Check if it has already been finalized 

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

901 # Ensure mag_y is set before any plot finalizes 

902 self._set_flux_default("mag_y") 

903 super().finalize() 

904 self._set_actions() 

905 name_short_x = self.config_mag_x.name_flux_short 

906 name_short_y = self.config_mag_y.name_flux_short 

907 

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

909 actionA, actionB = ( 

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

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

912 ) 

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

914 

915 if self.compute_chi: 

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

917 action_err = ( 

918 getattr(self.process.buildActions, key_err) 

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

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

921 ) 

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

923 else: 

924 # set to mmag 

925 self.process.buildActions.diff = MultiplyVector( 

926 actionA=action_diff, 

927 actionB=ConstantValue(value=1000.0), 

928 ) 

929 if not self.produce.plot.yAxisLabel: 

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

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

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

933 if self.unit is None: 

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

935 if self.name_prefix is None: 

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

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

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

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

940 

941 def get_key_flux_y(self) -> str: 

942 return self.mag_y 

943 

944 def reconfigure_dependent_magnitudes( 

945 self, 

946 key_flux_meas: str | None = None, 

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

948 ): 

949 if key_flux_meas is not None: 

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

951 

952 def setDefaults(self): 

953 super().setDefaults() 

954 self.produce.plot.yLims = self.limits_diff_mag_mmag_default 

955 

956 

957class MatchedRefCoaddDiffMagZoomTool(MatchedRefCoaddDiffMagTool): 

958 def setDefaults(self): 

959 super().setDefaults() 

960 self.produce.plot.yLims = self.limits_diff_mag_mmag_zoom_default 

961 self.produce.metric = NoMetric 

962 

963 

964class MatchedRefCoaddChiMagTool(MatchedRefCoaddDiffMagTool): 

965 def setDefaults(self): 

966 super().setDefaults() 

967 self.compute_chi = True 

968 self.produce.plot.yLims = self.limits_chi_default 

969 

970 

971class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffPlot): 

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

973 

974 coord_label = Field[str]( 

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

976 optional=True, 

977 default=None, 

978 ) 

979 coord_meas = Field[str]( 

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

981 optional=False, 

982 ) 

983 coord_ref = Field[str]( 

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

985 optional=False, 

986 ) 

987 coord_ref_cos = Field[str]( 

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

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

990 default=None, 

991 optional=True, 

992 ) 

993 coord_ref_cos_unit = Field[str]( 

994 doc="astropy unit of coord_ref_cos", 

995 default="deg", 

996 optional=True, 

997 ) 

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

999 # Default coords are in degrees and we want mas 

1000 scale_factor = Field[float]( 

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

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

1003 default=3600000, 

1004 ) 

1005 

1006 def finalize(self): 

1007 # Check if it has already been finalized 

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

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

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

1011 self._set_flux_default("mag_sn") 

1012 super().finalize() 

1013 self._set_actions() 

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

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

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

1017 name_short_x = self.config_mag_x.name_flux_short 

1018 name_short_y = self.config_mag_y.name_flux_short 

1019 

1020 if self.compute_chi: 

1021 self.process.buildActions.diff = DivideVector( 

1022 actionA=SubtractVector( 

1023 actionA=self.process.buildActions.pos_meas, 

1024 actionB=self.process.buildActions.pos_ref, 

1025 ), 

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

1027 ) 

1028 else: 

1029 factor = ConstantValue(value=self.scale_factor) 

1030 if self.coord_ref_cos: 

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

1032 factor = MultiplyVector( 

1033 actionA=factor, 

1034 actionB=CosVector( 

1035 actionA=MultiplyVector( 

1036 actionA=ConstantValue(value=factor_cos), 

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

1038 ) 

1039 ), 

1040 ) 

1041 self.process.buildActions.diff = MultiplyVector( 

1042 actionA=factor, 

1043 actionB=SubtractVector( 

1044 actionA=self.process.buildActions.pos_meas, 

1045 actionB=self.process.buildActions.pos_ref, 

1046 ), 

1047 ) 

1048 if self.unit is None: 

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

1050 if self.name_prefix is None: 

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

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

1053 self.name_prefix = ( 

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

1055 f"_{{name_type}}_" 

1056 ) 

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

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

1059 if not self.produce.plot.yAxisLabel: 

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

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

1062 self.produce.plot.yAxisLabel = ( 

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

1064 if self.compute_chi 

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

1066 ) 

1067 

1068 def get_key_flux_y(self) -> str: 

1069 return self.mag_sn 

1070 

1071 def reconfigure_dependent_magnitudes( 

1072 self, 

1073 key_flux_meas: str | None = None, 

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

1075 ): 

1076 if key_flux_meas is not None: 

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

1078 

1079 def setDefaults(self): 

1080 super().setDefaults() 

1081 self.produce.plot.yLims = self.limits_diff_pos_mas_default 

1082 

1083 

1084class MatchedRefCoaddDiffPositionZoomTool(MatchedRefCoaddDiffPositionTool): 

1085 def setDefaults(self): 

1086 super().setDefaults() 

1087 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1088 self.produce.metric = NoMetric 

1089 

1090 

1091class MatchedRefCoaddDiffCoordRaTool(MatchedRefCoaddDiffPositionTool): 

1092 def setDefaults(self): 

1093 super().setDefaults() 

1094 self.coord_meas = "coord_ra" 

1095 self.coord_ref = "ref_ra" 

1096 self.coord_ref_cos = "ref_dec" 

1097 

1098 

1099class MatchedRefCoaddDiffCoordRaZoomTool(MatchedRefCoaddDiffCoordRaTool): 

1100 def setDefaults(self): 

1101 super().setDefaults() 

1102 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1103 self.produce.metric = NoMetric 

1104 

1105 

1106class MatchedRefCoaddChiCoordRaTool(MatchedRefCoaddDiffCoordRaTool): 

1107 def setDefaults(self): 

1108 super().setDefaults() 

1109 self.compute_chi = True 

1110 self.produce.plot.yLims = self.limits_chi_default 

1111 

1112 

1113class MatchedRefCoaddDiffCoordDecTool(MatchedRefCoaddDiffPositionTool): 

1114 def setDefaults(self): 

1115 super().setDefaults() 

1116 self.coord_meas = "coord_dec" 

1117 self.coord_ref = "ref_dec" 

1118 

1119 

1120class MatchedRefCoaddDiffCoordDecZoomTool(MatchedRefCoaddDiffCoordDecTool): 

1121 def setDefaults(self): 

1122 super().setDefaults() 

1123 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default 

1124 self.produce.metric = NoMetric 

1125 

1126 

1127class MatchedRefCoaddChiCoordDecTool(MatchedRefCoaddDiffCoordDecTool): 

1128 def setDefaults(self): 

1129 super().setDefaults() 

1130 self.compute_chi = True 

1131 self.produce.plot.yLims = self.limits_chi_default 

1132 

1133 

1134class MatchedRefCoaddDiffDistanceTool(MatchedRefCoaddDiffPlot): 

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

1136 objects.""" 

1137 

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

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

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

1141 # Default coords are in degrees and we want mas 

1142 scale_factor = Field[float]( 

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

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

1145 default=3600000, 

1146 ) 

1147 

1148 def finalize(self): 

1149 # Check if it has already been finalized 

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

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

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

1153 self._set_flux_default("mag_sn") 

1154 super().finalize() 

1155 self._set_actions() 

1156 

1157 name_short_x = self.config_mag_x.name_flux_short 

1158 name_short_y = self.config_mag_y.name_flux_short 

1159 

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

1161 if self.compute_chi: 

1162 self.process.buildActions.diff = DivideVector( 

1163 actionA=self.process.buildActions.dist, 

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

1165 ) 

1166 else: 

1167 self.process.buildActions.diff = MultiplyVector( 

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

1169 actionB=self.process.buildActions.dist, 

1170 ) 

1171 if self.unit is None: 

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

1173 if self.name_prefix is None: 

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

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

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

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

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

1179 if not self.produce.plot.yAxisLabel: 

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

1181 self.produce.plot.yAxisLabel = ( 

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

1183 ) 

1184 

1185 def get_key_flux_y(self) -> str: 

1186 return self.mag_sn 

1187 

1188 def reconfigure_dependent_magnitudes( 

1189 self, 

1190 key_flux_meas: str | None = None, 

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

1192 ): 

1193 if key_flux_meas is not None: 

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

1195 

1196 def setDefaults(self): 

1197 super().setDefaults() 

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

1199 

1200 

1201class MatchedRefCoaddChiDistanceTool(MatchedRefCoaddDiffDistanceTool): 

1202 def setDefaults(self): 

1203 super().setDefaults() 

1204 self.compute_chi = True 

1205 self.produce.plot.yLims = self.limits_chi_default 

1206 

1207 

1208class MatchedRefCoaddDiffDistanceZoomTool(MatchedRefCoaddDiffDistanceTool): 

1209 def setDefaults(self): 

1210 super().setDefaults() 

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

1212 self.produce.metric = NoMetric 

1213 

1214 

1215def reconfigure_diff_matched_defaults( 

1216 config: AnalysisBaseConfig | None = None, 

1217 context: str | None = None, 

1218 key_flux_meas: str | None = None, 

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

1220 use_any: bool | None = None, 

1221 use_galaxies: bool | None = None, 

1222 use_stars: bool | None = None, 

1223): 

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

1225 and all of its subclasses. 

1226 

1227 Parameters 

1228 ---------- 

1229 config 

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

1231 member MatchedRefCoaddTool atools. 

1232 context 

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

1234 MatchedRefCoaddTool.context. 

1235 key_flux_meas 

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

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

1238 configurations that load error keys as well as fluxes. 

1239 bands_color 

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

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

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

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

1244 use_any 

1245 Whether to compute metrics for objects of all types. 

1246 use_galaxies 

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

1248 use_stars 

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

1250 

1251 Notes 

1252 ----- 

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

1254 """ 

1255 if key_flux_meas is not None: 

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

1257 if key_flux_meas not in keys_flux: 

1258 key_flux_err = f"{key_flux_meas}_err" 

1259 if key_flux_err not in keys_flux: 

1260 raise ValueError( 

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

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

1263 ) 

1264 key_flux_meas = key_flux_err 

1265 

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

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

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

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

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

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

1272 

1273 def all_subclasses(cls): 

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

1275 

1276 subclasses = all_subclasses(MatchedRefCoaddTool) 

1277 

1278 """ 

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

1280 

1281 context (DC2, source_injection, etc) 

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

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

1284 field without having to implement separate context functions. 

1285 key_flux_meas 

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

1287 onto an AnalysisContext instead. 

1288 bands_color 

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

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

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

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

1293 so no obvious alternative exists. 

1294 use_* 

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

1296 booleans rather than exclusive choices. 

1297 """ 

1298 

1299 # This sets defaults for all known subclasses 

1300 for tool in subclasses: 

1301 tool.reconfigure( 

1302 tool, 

1303 context=context, 

1304 key_flux_meas=key_flux_meas, 

1305 bands_color=bands_color, 

1306 use_any=use_any, 

1307 use_galaxies=use_galaxies, 

1308 use_stars=use_stars, 

1309 ) 

1310 

1311 # This sets defaults for all existing tools 

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

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

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

1315 if config is not None: 

1316 for tool in config.atools: 

1317 if isinstance(tool, MatchedRefCoaddTool): 

1318 tool: MatchedRefCoaddTool = tool 

1319 tool.reconfigure( 

1320 context=context, 

1321 key_flux_meas=key_flux_meas, 

1322 bands_color=bands_color, 

1323 use_any=use_any, 

1324 use_galaxies=use_galaxies, 

1325 use_stars=use_stars, 

1326 )