Coverage for python / lsst / analysis / tools / atools / diffMatched.py: 20%
543 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:36 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:36 +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
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
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
96def _set_field_config(config: pexConfig.Config | pexConfig.ConfigMeta, name: str, value):
97 """Set the value of a Config Field or ConfigMeta default value.
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)
114class MatchedRefCoaddTool(ObjectClassTool):
115 """Base tool for matched-to-reference metrics/plots on coadds.
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.
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).
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 """
135 _suffix_ref = "_ref"
136 _suffix_target = "_target"
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 )
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 )
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 )
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)
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__}")
198 # Other tools will except selector_all
199 self.selection_suffix = self._suffix_ref if self.select_ref_by_default else self._suffix_target
201 super().finalize()
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)
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
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.
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.
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)
265 # This allows the method to work automatically on class defaults
266 kwargs = {"self": self} if inspect.isclass(self) else {}
268 # Change any dependent magnitudes
269 self.reconfigure_dependent_magnitudes(key_flux_meas=key_flux_meas, bands_color=bands_color, **kwargs)
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.
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 """
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
297class MatchedRefCoaddDiffTool(MagnitudeXTool, MatchedRefCoaddTool):
298 """Base tool for generic diffs between reference and measured values."""
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)
310 compute_chi = pexConfig.Field[bool](
311 default=False,
312 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
313 )
315 def _set_actions(self, suffix=None):
316 if suffix is None:
317 suffix = ""
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()}"
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)
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 )
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.
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".
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 = ""
379 if unit_select is None:
380 unit_select = "mag"
382 key_flux = self.config_mag_x.key_flux
384 units = {}
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}"
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)
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)
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
421 @property
422 def config_mag_y(self):
423 """Return the y-axis magnitude config.
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]
435 def finalize(self):
436 MagnitudeXTool.finalize(self)
437 MatchedRefCoaddTool.finalize(self)
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")
444 def setDefaults(self):
445 MagnitudeXTool.setDefaults(self)
446 MatchedRefCoaddTool.setDefaults(self)
447 self.mag_x = "ref_matched"
448 self.prep.selectors.matched = MatchedObjectSelector()
451class MatchedRefCoaddDiffPlot(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
452 """Base tool for generic diffs between reference and measured values,
453 with a scatter plot."""
455 def do_metrics(self):
456 return not isinstance(self.produce.metric, NoMetric)
458 def get_key_flux_y(self) -> str:
459 return super().get_key_flux_y()
461 def finalize(self):
462 MatchedRefCoaddDiffTool.finalize(self)
463 MagnitudeScatterPlot.finalize(self)
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
473class MatchedRefCoaddCompurityTool(MagnitudeTool, MatchedRefCoaddTool):
474 """Plot the fraction of injected sources recovered by input magnitude.
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 """
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 )
507 @property
508 def config_mag_ref(self):
509 return self._config_mag("mag_ref")
511 @property
512 def config_mag_target(self):
513 return self._config_mag("mag_target")
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")
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 = {}
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
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)
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()
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}"
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 )
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 )
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 )
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
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 )
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 )
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)
684 def setDefaults(self):
685 MagnitudeTool.setDefaults(self)
686 MatchedRefCoaddTool.setDefaults(self)
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}"
695class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffPlot):
696 """Tool for diffs between reference and measured coadd mags.
698 Notes
699 -----
700 Since this tool requires at least two bands, it is essentially impossible
701 to call on its own.
702 """
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")
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 []
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
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
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()
751 self.unit = "" if self.compute_chi else "mmag"
752 subtype = "chi" if self.compute_chi else "diff"
754 metric_base = self.produce.metric
755 metric = metric_base
756 plot_base = self.produce.plot
758 do_metrics = self.do_metrics()
760 actions_metric = {}
761 actions_plot = {}
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
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)
783 plot = copy.copy(plot_base)
785 plot.suffix_y = suffix_y
786 plot.suffix_stat = suffix_y
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}"
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 )
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)
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
832 def get_key_flux_y(self) -> str:
833 return self.mag_y1
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)
858 def setDefaults(self):
859 super().setDefaults()
860 self.produce.plot.yLims = self.limits_diff_color_mmag_default
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))
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
880class MatchedRefCoaddChiColorTool(MatchedRefCoaddDiffColorTool):
881 def setDefaults(self):
882 super().setDefaults()
883 self.compute_chi = True
884 self.produce.plot.yLims = self.limits_chi_default
887class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffPlot):
888 """Tool for diffs between reference and measured coadd mags."""
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 )
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
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)
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()
941 def get_key_flux_y(self) -> str:
942 return self.mag_y
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)
952 def setDefaults(self):
953 super().setDefaults()
954 self.produce.plot.yLims = self.limits_diff_mag_mmag_default
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
964class MatchedRefCoaddChiMagTool(MatchedRefCoaddDiffMagTool):
965 def setDefaults(self):
966 super().setDefaults()
967 self.compute_chi = True
968 self.produce.plot.yLims = self.limits_chi_default
971class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffPlot):
972 """Tool for diffs between reference and measured coadd positions."""
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 )
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
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 )
1068 def get_key_flux_y(self) -> str:
1069 return self.mag_sn
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)
1079 def setDefaults(self):
1080 super().setDefaults()
1081 self.produce.plot.yLims = self.limits_diff_pos_mas_default
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
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"
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
1106class MatchedRefCoaddChiCoordRaTool(MatchedRefCoaddDiffCoordRaTool):
1107 def setDefaults(self):
1108 super().setDefaults()
1109 self.compute_chi = True
1110 self.produce.plot.yLims = self.limits_chi_default
1113class MatchedRefCoaddDiffCoordDecTool(MatchedRefCoaddDiffPositionTool):
1114 def setDefaults(self):
1115 super().setDefaults()
1116 self.coord_meas = "coord_dec"
1117 self.coord_ref = "ref_dec"
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
1127class MatchedRefCoaddChiCoordDecTool(MatchedRefCoaddDiffCoordDecTool):
1128 def setDefaults(self):
1129 super().setDefaults()
1130 self.compute_chi = True
1131 self.produce.plot.yLims = self.limits_chi_default
1134class MatchedRefCoaddDiffDistanceTool(MatchedRefCoaddDiffPlot):
1135 """Tool for distances between matched reference and measured coadd
1136 objects."""
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 )
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()
1157 name_short_x = self.config_mag_x.name_flux_short
1158 name_short_y = self.config_mag_y.name_flux_short
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 )
1185 def get_key_flux_y(self) -> str:
1186 return self.mag_sn
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)
1196 def setDefaults(self):
1197 super().setDefaults()
1198 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_default[1]]
1201class MatchedRefCoaddChiDistanceTool(MatchedRefCoaddDiffDistanceTool):
1202 def setDefaults(self):
1203 super().setDefaults()
1204 self.compute_chi = True
1205 self.produce.plot.yLims = self.limits_chi_default
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
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.
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.
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
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"
1273 def all_subclasses(cls):
1274 return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
1276 subclasses = all_subclasses(MatchedRefCoaddTool)
1278 """
1279 Further context on these kwargs (get it?) and why they're not contexts:
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 """
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 )
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 )