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
« 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
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)
46import copy
47import inspect
48from abc import abstractmethod
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
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
95def _set_field_config(config: pexConfig.Config | pexConfig.ConfigMeta, name: str, value):
96 """Set the value of a Config Field or ConfigMeta default value.
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)
113class MatchedRefCoaddTool(ObjectClassTool):
114 """Base tool for matched-to-reference metrics/plots on coadds.
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.
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).
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 """
134 _suffix_ref = "_ref"
135 _suffix_target = "_target"
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 )
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 )
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 )
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)
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__}")
197 # Other tools will except selector_all
198 self.selection_suffix = self._suffix_ref if self.select_ref_by_default else self._suffix_target
200 super().finalize()
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)
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
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.
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.
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)
264 # This allows the method to work automatically on class defaults
265 kwargs = {"self": self} if inspect.isclass(self) else {}
267 # Change any dependent magnitudes
268 self.reconfigure_dependent_magnitudes(key_flux_meas=key_flux_meas, bands_color=bands_color, **kwargs)
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.
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 """
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
296class MatchedRefCoaddDiffTool(MagnitudeXTool, MatchedRefCoaddTool):
297 """Base tool for generic diffs between reference and measured values."""
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)
309 compute_chi = pexConfig.Field[bool](
310 default=False,
311 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
312 )
314 def _set_actions(self, suffix=None):
315 if suffix is None:
316 suffix = ""
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()}"
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)
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 )
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.
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".
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 = ""
378 if unit_select is None:
379 unit_select = "mag"
381 key_flux = self.config_mag_x.key_flux
383 units = {}
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}"
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)
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)
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
420 @property
421 def config_mag_y(self):
422 """Return the y-axis magnitude config.
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]
434 def finalize(self):
435 MagnitudeXTool.finalize(self)
436 MatchedRefCoaddTool.finalize(self)
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")
443 def setDefaults(self):
444 MagnitudeXTool.setDefaults(self)
445 MatchedRefCoaddTool.setDefaults(self)
446 self.mag_x = "ref_matched"
447 self.prep.selectors.matched = MatchedObjectSelector()
450class MatchedRefCoaddDiffPlot(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
451 """Base tool for generic diffs between reference and measured values,
452 with a scatter plot."""
454 def do_metrics(self):
455 return not isinstance(self.produce.metric, NoMetric)
457 def get_key_flux_y(self) -> str:
458 return super().get_key_flux_y()
460 def finalize(self):
461 MatchedRefCoaddDiffTool.finalize(self)
462 MagnitudeScatterPlot.finalize(self)
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
472class MatchedRefCoaddCompurityTool(MagnitudeTool, MatchedRefCoaddTool):
473 """Plot the fraction of injected sources recovered by input magnitude.
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 """
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 )
506 @property
507 def config_mag_ref(self):
508 return self._config_mag("mag_ref")
510 @property
511 def config_mag_target(self):
512 return self._config_mag("mag_target")
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")
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 = {}
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
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)
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()
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}"
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 )
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 )
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 )
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
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 )
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 )
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)
683 def setDefaults(self):
684 MagnitudeTool.setDefaults(self)
685 MatchedRefCoaddTool.setDefaults(self)
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}"
694class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffPlot):
695 """Tool for diffs between reference and measured coadd mags.
697 Notes
698 -----
699 Since this tool requires at least two bands, it is essentially impossible
700 to call on its own.
701 """
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")
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 []
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
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
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()
750 self.unit = "" if self.compute_chi else "mmag"
751 subtype = "chi" if self.compute_chi else "diff"
753 metric_base = self.produce.metric
754 metric = metric_base
755 plot_base = self.produce.plot
757 do_metrics = self.do_metrics()
759 actions_metric = {}
760 actions_plot = {}
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
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)
782 plot = copy.copy(plot_base)
784 plot.suffix_y = suffix_y
785 plot.suffix_stat = suffix_y
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}"
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 )
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)
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
831 def get_key_flux_y(self) -> str:
832 return self.mag_y1
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)
857 def setDefaults(self):
858 super().setDefaults()
859 self.produce.plot.yLims = self.limits_diff_color_mmag_default
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))
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
879class MatchedRefCoaddChiColorTool(MatchedRefCoaddDiffColorTool):
880 def setDefaults(self):
881 super().setDefaults()
882 self.compute_chi = True
883 self.produce.plot.yLims = self.limits_chi_default
886class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffPlot):
887 """Tool for diffs between reference and measured coadd mags."""
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 )
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
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)
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()
940 def get_key_flux_y(self) -> str:
941 return self.mag_y
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)
951 def setDefaults(self):
952 super().setDefaults()
953 self.produce.plot.yLims = self.limits_diff_mag_mmag_default
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
963class MatchedRefCoaddChiMagTool(MatchedRefCoaddDiffMagTool):
964 def setDefaults(self):
965 super().setDefaults()
966 self.compute_chi = True
967 self.produce.plot.yLims = self.limits_chi_default
970class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffPlot):
971 """Tool for diffs between reference and measured coadd positions."""
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 )
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
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 )
1067 def get_key_flux_y(self) -> str:
1068 return self.mag_sn
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)
1078 def setDefaults(self):
1079 super().setDefaults()
1080 self.produce.plot.yLims = self.limits_diff_pos_mas_default
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
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"
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
1105class MatchedRefCoaddChiCoordRaTool(MatchedRefCoaddDiffCoordRaTool):
1106 def setDefaults(self):
1107 super().setDefaults()
1108 self.compute_chi = True
1109 self.produce.plot.yLims = self.limits_chi_default
1112class MatchedRefCoaddDiffCoordDecTool(MatchedRefCoaddDiffPositionTool):
1113 def setDefaults(self):
1114 super().setDefaults()
1115 self.coord_meas = "coord_dec"
1116 self.coord_ref = "ref_dec"
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
1126class MatchedRefCoaddChiCoordDecTool(MatchedRefCoaddDiffCoordDecTool):
1127 def setDefaults(self):
1128 super().setDefaults()
1129 self.compute_chi = True
1130 self.produce.plot.yLims = self.limits_chi_default
1133class MatchedRefCoaddDiffDistanceTool(MatchedRefCoaddDiffPlot):
1134 """Tool for distances between matched reference and measured coadd
1135 objects."""
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 )
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()
1156 name_short_x = self.config_mag_x.name_flux_short
1157 name_short_y = self.config_mag_y.name_flux_short
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 )
1184 def get_key_flux_y(self) -> str:
1185 return self.mag_sn
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)
1195 def setDefaults(self):
1196 super().setDefaults()
1197 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_default[1]]
1200class MatchedRefCoaddChiDistanceTool(MatchedRefCoaddDiffDistanceTool):
1201 def setDefaults(self):
1202 super().setDefaults()
1203 self.compute_chi = True
1204 self.produce.plot.yLims = self.limits_chi_default
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
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.
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.
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
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"
1272 def all_subclasses(cls):
1273 return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
1275 subclasses = all_subclasses(MatchedRefCoaddTool)
1277 """
1278 Further context on these kwargs (get it?) and why they're not contexts:
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 """
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 )
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 )