Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 72%
552 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 09:58 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 09:58 +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": 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true
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": 217 ↛ exitline 217 didn't return from function 'get_selector_ref' because the pattern on line 217 always matched
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: 372 ↛ 374line 372 didn't jump to line 374 because the condition on line 372 was always true
373 unit = self.unit if self.unit is not None else ""
374 if name_prefix is None: 374 ↛ 376line 374 didn't jump to line 376 because the condition on line 374 was always true
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: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
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: 407 ↛ 409line 407 didn't jump to line 409 because the condition on line 407 was always true
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: 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true
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: 516 ↛ exitline 516 didn't return from function 'finalize' because the condition on line 516 was always true
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: 559 ↛ 562line 559 didn't jump to line 562 because the condition on line 559 was always true
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: 570 ↛ 573line 570 didn't jump to line 573 because the condition on line 570 was always true
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: 664 ↛ 562line 664 didn't jump to line 562 because the condition on line 664 was always true
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 plot_action = CompletenessHist(
671 action=completeness_plot,
672 )
673 # Since the plot action is made on the fly, it can't be
674 # configured in a pipeline yaml. This makes the keys
675 # formattable, but (unfortunately) in a way that's hard
676 # to discover or document.
677 for label_type in ("ref", "target"):
678 name_mag_label = f"mag_{label_type}_label"
679 label = getattr(plot_action, name_mag_label)
680 if "{name_flux}" in label: 680 ↛ 677line 680 didn't jump to line 677 because the condition on line 680 was always true
681 name_flux = getattr(self, f"config_mag_{label_type}").name_flux
682 # Still no support for partial formatting, i.e.
683 # if {band} is in label and it's not a kwarg of
684 # format, it will raise a KeyError
685 kwargs_format = {"band": "{band}"} if "{band}" in label else {}
686 label = label.format(name_flux=name_flux, **kwargs_format)
687 setattr(plot_action, name_mag_label, label)
688 setattr(
689 self.produce.plot.actions,
690 object_class,
691 plot_action,
692 )
694 def reconfigure_dependent_magnitudes(
695 self,
696 key_flux_meas: str | None = None,
697 bands_color: dict[str, str] | list[str] | None = None,
698 ):
699 if key_flux_meas is not None:
700 _set_field_config(self, name="mag_target", value=key_flux_meas)
702 def setDefaults(self):
703 MagnitudeTool.setDefaults(self)
704 MatchedRefCoaddTool.setDefaults(self)
706 self.mag_bins_plot.mag_interval = 100
707 self.mag_bins_plot.mag_width = 200
708 # Completeness/purity don't need a ref/target suffix as they are by
709 # definition a function of ref/target mags, respectively
710 self.name_suffix = "_mag{name_mag}"
713class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffPlot):
714 """Tool for diffs between reference and measured coadd mags.
716 Notes
717 -----
718 Since this tool requires at least two bands, it is essentially impossible
719 to call on its own.
720 """
722 mag_y1 = Field[str](default="cmodel_err", doc="Flux field for first magnitude")
723 mag_y2 = Field[str](
724 doc="Flux field for second magnitude (to subtract from first); default same as first",
725 default=None,
726 optional=True,
727 )
728 bands = DictField[str, str](
729 doc="Bands for colors. ",
730 # The empty value for y is needed to indicate that it's a valid band
731 default={"u": "g", "g": "r,i", "r": "i", "i": "z", "z": "y", "y": ""},
732 )
733 band_separator = Field[str](default=",", doc="Separator for multiple bands")
735 def _split_bands(self, band_list: str):
736 # Split returns [""] for an empty string
737 return band_list.split(self.band_separator) if band_list else []
739 def finalize(self):
740 # Check if it has already been finalized
741 if not hasattr(self.process.buildActions, "diff_0"): 741 ↛ exitline 741 didn't return from function 'finalize' because the condition on line 741 was always true
742 if self.mag_y2 is None: 742 ↛ 743line 742 didn't jump to line 743 because the condition on line 742 was never true
743 self.mag_y2 = self.mag_y1
744 # Ensure mag_y1/2 are set before any plot finalizes
745 # This may result in duplicate actions but these are just plain
746 # column selectors so that's not a serious problem
747 bands = {band1: self._split_bands(band2_list) for band1, band2_list in self.bands.items()}
748 n_bands = 0
750 # Set up mag actions for every band needed before finalizing plots
751 for band1, band2_list in bands.items():
752 for band2 in band2_list:
753 mag_y1 = f"mag_y_{band1}"
754 mag_y2 = f"mag_y_{band2}"
755 mag_x1 = f"mag_x_{band1}"
756 mag_x2 = f"mag_x_{band2}"
757 self._set_flux_default(mag_y1, band=band1, name_mag=self.mag_y1)
758 self._set_flux_default(mag_y2, band=band2, name_mag=self.mag_y2)
759 self._set_flux_default(mag_x1, band=band1, name_mag=self.mag_x)
760 self._set_flux_default(mag_x2, band=band2, name_mag=self.mag_x)
761 n_bands += 1
763 # These two lines must appear in this order so that every color
764 # has its plot actions finalized with a suffix (i.e., pointing
765 # summary stats at yStars_0 instead of yStars).
766 self.suffixes_y_finalize = [f"_{idx}" for idx in range(n_bands)]
767 super().finalize()
769 self.unit = "" if self.compute_chi else "mmag"
770 subtype = "chi" if self.compute_chi else "diff"
772 metric_base = self.produce.metric
773 metric = metric_base
774 plot_base = self.produce.plot
776 do_metrics = self.do_metrics()
778 actions_metric = {}
779 actions_plot = {}
781 config_mag_x = self.config_mag_x
782 config_mag_y = self.config_mag_y
783 name_short_x = config_mag_x.name_flux_short
784 name_short_y = config_mag_y.name_flux_short
786 idx = 0
787 for band1, band2_list in bands.items():
788 for band2 in band2_list:
789 name_color = f"{band1}_minus_{band2}"
790 # Keep this index-based to simplify finalize
791 suffix_y = f"_{idx}"
792 self._set_actions(suffix=suffix_y)
793 self.name_prefix = (
794 f"photom_{name_short_y}_vs_{name_short_x}_color_{name_color}"
795 f"_{subtype}_{{name_type}}_"
796 )
797 if do_metrics: 797 ↛ 801line 797 didn't jump to line 801 because the condition on line 797 was always true
798 metric = copy.copy(metric_base)
799 metric.units = self.configureMetrics(attr_suffix=suffix_y)
801 plot = copy.copy(plot_base)
803 plot.suffix_y = suffix_y
804 plot.suffix_stat = suffix_y
806 mag_y1 = f"{self.mag_y1}_{band1}"
807 mag_y2 = f"{self.mag_y2}_{band2}"
808 mag_x1 = f"{self.mag_x}_{band1}"
809 mag_x2 = f"{self.mag_x}_{band2}"
811 diff = ColorDiff(
812 color1_flux1=getattr(self.process.buildActions, f"flux_{mag_y1}"),
813 color1_flux2=getattr(self.process.buildActions, f"flux_{mag_y2}"),
814 color2_flux1=getattr(self.process.buildActions, f"flux_{mag_x1}"),
815 color2_flux2=getattr(self.process.buildActions, f"flux_{mag_x2}"),
816 )
818 if self.compute_chi:
819 diff = DivideVector(
820 actionA=diff,
821 actionB=ColorError(
822 flux_err1=DivideVector(
823 actionA=getattr(self.process.buildActions, f"flux_err_{mag_y1}"),
824 actionB=getattr(self.process.buildActions, f"flux_{mag_y1}"),
825 ),
826 flux_err2=DivideVector(
827 actionA=getattr(self.process.buildActions, f"flux_err_{mag_y2}"),
828 actionB=getattr(self.process.buildActions, f"flux_{mag_y2}"),
829 ),
830 ),
831 )
832 setattr(self.process.buildActions, f"diff{plot.suffix_y}", diff)
834 label = f"({band1} - {band2}) ({config_mag_y.name_flux} - {config_mag_x.name_flux})"
835 label = f"χ = ({label})/σ" if self.compute_chi else f"{label} (mmag)"
836 plot.yAxisLabel = label
837 actions_metric[name_color] = metric
838 actions_plot[name_color] = plot
839 idx += 1
840 if do_metrics: 840 ↛ 845line 840 didn't jump to line 845 because the condition on line 840 was always true
841 action_metric = StructMetricAction()
842 for name_action, action in actions_metric.items():
843 setattr(action_metric.actions, name_action, action)
844 self.produce.metric = action_metric
845 action_plot = StructPlotAction()
846 for name_action, action in actions_plot.items():
847 setattr(action_plot.actions, name_action, action)
848 self.produce.plot = action_plot
850 def get_key_flux_y(self) -> str:
851 return self.mag_y1
853 def reconfigure_dependent_magnitudes(
854 self,
855 key_flux_meas: str | None = None,
856 bands_color: dict[str, str] | list[str] | None = None,
857 ):
858 if key_flux_meas is not None:
859 _set_field_config(self, name="mag_y1", value=key_flux_meas)
860 if bands_color is not None:
861 if isinstance(bands_color, dict):
862 _set_field_config(self, name="bands", value=bands_color)
863 else:
864 bands_new = {}
865 bands_old = self.bands.default if inspect.isclass(self) else self.bands
866 for band in bands_color:
867 colors = bands_old.get(band)
868 if colors is None:
869 raise ValueError(
870 f"Passed {bands_color=} to reconfigure colors for {self=} but {band=}"
871 f" is not in {bands_old=}."
872 )
873 bands_new[band] = ",".join(band for band in colors.split(",") if band in bands_color)
874 _set_field_config(self, name="bands", value=bands_new)
876 def setDefaults(self):
877 super().setDefaults()
878 self.produce.plot.yLims = self.limits_diff_color_mmag_default
880 def validate(self):
881 super().validate()
882 errors = []
883 for band1, band2_list in self.bands.items():
884 bands = self._split_bands(band2_list)
885 if len(set(bands)) != len(bands):
886 errors.append(f"value={band2_list} is not a set for key={band1}")
887 if errors:
888 raise ValueError("\n".join(errors))
891class MatchedRefCoaddDiffColorZoomTool(MatchedRefCoaddDiffColorTool):
892 def setDefaults(self):
893 super().setDefaults()
894 self.produce.plot.yLims = self.limits_diff_color_mmag_zoom_default
895 self.produce.metric = NoMetric
898class MatchedRefCoaddChiColorTool(MatchedRefCoaddDiffColorTool):
899 def setDefaults(self):
900 super().setDefaults()
901 self.compute_chi = True
902 self.produce.plot.yLims = self.limits_chi_default
905class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffPlot):
906 """Tool for diffs between reference and measured coadd mags."""
908 mag_y = pexConfig.Field[str](
909 default="cmodel_err",
910 doc="Flux (magnitude) pexConfig.Field to difference against the x-axis values",
911 )
912 measure_y_minus_x = pexConfig.Field[bool](
913 default=True, doc="Whether to plot the y-axis magnitude minus the x-axis; otherwise x-y if False."
914 )
916 def finalize(self):
917 # Check if it has already been finalized
918 if not hasattr(self.process.buildActions, "diff"): 918 ↛ exitline 918 didn't return from function 'finalize' because the condition on line 918 was always true
919 # Ensure mag_y is set before any plot finalizes
920 self._set_flux_default("mag_y")
921 super().finalize()
922 self._set_actions()
923 name_short_x = self.config_mag_x.name_flux_short
924 name_short_y = self.config_mag_y.name_flux_short
926 prefix_action = "flux" if self.compute_chi else "mag"
927 actionA, actionB = (
928 getattr(self.process.buildActions, f"{prefix_action}_{mag}")
929 for mag in ((self.mag_y, self.mag_x) if self.measure_y_minus_x else (self.mag_x, self.mag_y))
930 )
931 action_diff = SubtractVector(actionA=actionA, actionB=actionB)
933 if self.compute_chi:
934 key_err = f"flux_err_{self.mag_y}"
935 action_err = (
936 getattr(self.process.buildActions, key_err)
937 if hasattr(self.process.buildActions, key_err)
938 else getattr(self.process.buildActions, f"flux_err_{self.mag_x}")
939 )
940 self.process.buildActions.diff = DivideVector(actionA=action_diff, actionB=action_err)
941 else:
942 # set to mmag
943 self.process.buildActions.diff = MultiplyVector(
944 actionA=action_diff,
945 actionB=ConstantValue(value=1000.0),
946 )
947 if not self.produce.plot.yAxisLabel: 947 ↛ 951line 947 didn't jump to line 951 because the condition on line 947 was always true
948 label_x, label_y = (mag.name_flux for mag in (self.config_mag_x, self.config_mag_y))
949 label = f"{label_y} - {label_x}" if self.measure_y_minus_x else f"{label_x} - {label_y}"
950 self.produce.plot.yAxisLabel = f"χ = ({label})/σ" if self.compute_chi else f"{label} (mmag)"
951 if self.unit is None: 951 ↛ 952line 951 didn't jump to line 952 because the condition on line 951 was never true
952 self.unit = "" if self.compute_chi else "mmag"
953 if self.name_prefix is None: 953 ↛ 954line 953 didn't jump to line 954 because the condition on line 953 was never true
954 subtype = "chi" if self.compute_chi else "diff"
955 self.name_prefix = f"photom_{name_short_y}_vs_{name_short_x}_mag_{subtype}_{{name_type}}_"
956 if self.do_metrics() and not self.produce.metric.units: 956 ↛ exitline 956 didn't return from function 'finalize' because the condition on line 956 was always true
957 self.produce.metric.units = self.configureMetrics()
959 def get_key_flux_y(self) -> str:
960 return self.mag_y
962 def reconfigure_dependent_magnitudes(
963 self,
964 key_flux_meas: str | None = None,
965 bands_color: dict[str, str] | None = None,
966 ):
967 if key_flux_meas is not None:
968 _set_field_config(self, name="mag_y", value=key_flux_meas)
970 def setDefaults(self):
971 super().setDefaults()
972 self.produce.plot.yLims = self.limits_diff_mag_mmag_default
975class MatchedRefCoaddDiffMagZoomTool(MatchedRefCoaddDiffMagTool):
976 def setDefaults(self):
977 super().setDefaults()
978 self.produce.plot.yLims = self.limits_diff_mag_mmag_zoom_default
979 self.produce.metric = NoMetric
982class MatchedRefCoaddChiMagTool(MatchedRefCoaddDiffMagTool):
983 def setDefaults(self):
984 super().setDefaults()
985 self.compute_chi = True
986 self.produce.plot.yLims = self.limits_chi_default
989class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffPlot):
990 """Tool for diffs between reference and measured coadd positions."""
992 coord_label = Field[str](
993 doc="The plot label for the astrometric variable (default coord_meas)",
994 optional=True,
995 default=None,
996 )
997 coord_meas = Field[str](
998 doc="The key for measured values of the astrometric variable",
999 optional=False,
1000 )
1001 coord_ref = Field[str](
1002 doc="The key for reference values of the astrometric variable",
1003 optional=False,
1004 )
1005 coord_ref_cos = Field[str](
1006 doc="The key for reference values of the cosine correction astrometric variable"
1007 " (i.e. dec if coord_meas is RA)",
1008 default=None,
1009 optional=True,
1010 )
1011 coord_ref_cos_unit = Field[str](
1012 doc="astropy unit of coord_ref_cos",
1013 default="deg",
1014 optional=True,
1015 )
1016 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
1017 # Default coords are in degrees and we want mas
1018 scale_factor = Field[float](
1019 doc="The factor to multiply distances by (e.g. the pixel scale if coordinates have pixel units or "
1020 "the desired spherical coordinate unit conversion otherwise)",
1021 default=3600000,
1022 )
1024 def finalize(self):
1025 # Check if it has already been finalized
1026 if not hasattr(self.process.buildActions, "diff"): 1026 ↛ exitline 1026 didn't return from function 'finalize' because the condition on line 1026 was always true
1027 # Set before MagnitudeScatterPlot.finalize to undo PSF default.
1028 # Matched ref tables may not have PSF fluxes, or prefer CModel.
1029 self._set_flux_default("mag_sn")
1030 super().finalize()
1031 self._set_actions()
1032 name = self.coord_label if self.coord_label else self.coord_meas
1033 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.coord_meas)
1034 self.process.buildActions.pos_ref = LoadVector(vectorKey=self.coord_ref)
1035 name_short_x = self.config_mag_x.name_flux_short
1036 name_short_y = self.config_mag_y.name_flux_short
1038 if self.compute_chi:
1039 self.process.buildActions.diff = DivideVector(
1040 actionA=SubtractVector(
1041 actionA=self.process.buildActions.pos_meas,
1042 actionB=self.process.buildActions.pos_ref,
1043 ),
1044 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"),
1045 )
1046 else:
1047 factor = ConstantValue(value=self.scale_factor)
1048 if self.coord_ref_cos: 1048 ↛ 1049line 1048 didn't jump to line 1049 because the condition on line 1048 was never true
1049 factor_cos = u.Unit(self.coord_ref_cos_unit).to(u.rad)
1050 factor = MultiplyVector(
1051 actionA=factor,
1052 actionB=CosVector(
1053 actionA=MultiplyVector(
1054 actionA=ConstantValue(value=factor_cos),
1055 actionB=LoadVector(vectorKey=self.coord_ref_cos),
1056 )
1057 ),
1058 )
1059 self.process.buildActions.diff = MultiplyVector(
1060 actionA=factor,
1061 actionB=SubtractVector(
1062 actionA=self.process.buildActions.pos_meas,
1063 actionB=self.process.buildActions.pos_ref,
1064 ),
1065 )
1066 if self.unit is None: 1066 ↛ 1068line 1066 didn't jump to line 1068 because the condition on line 1066 was always true
1067 self.unit = "" if self.compute_chi else "mas"
1068 if self.name_prefix is None: 1068 ↛ 1075line 1068 didn't jump to line 1075 because the condition on line 1068 was always true
1069 subtype = "chi" if self.compute_chi else "diff"
1070 coord_prefix = "" if "coord" in self.coord_meas else "coord_"
1071 self.name_prefix = (
1072 f"astrom_{name_short_y}_vs_{name_short_x}_{coord_prefix}{self.coord_meas}_{subtype}"
1073 f"_{{name_type}}_"
1074 )
1075 if self.do_metrics() and not self.produce.metric.units: 1075 ↛ 1077line 1075 didn't jump to line 1077 because the condition on line 1075 was always true
1076 self.produce.metric.units = self.configureMetrics()
1077 if not self.produce.plot.yAxisLabel: 1077 ↛ exitline 1077 didn't return from function 'finalize' because the condition on line 1077 was always true
1078 label = f"({name_short_y} - {name_short_x})"
1079 coord_suffix = "" if "coord" in name else " coord"
1080 self.produce.plot.yAxisLabel = (
1081 f"χ = ({label} {name}{coord_suffix})/σ"
1082 if self.compute_chi
1083 else f"{label} {name}{coord_suffix} ({self.unit})"
1084 )
1086 def get_key_flux_y(self) -> str:
1087 return self.mag_sn
1089 def reconfigure_dependent_magnitudes(
1090 self,
1091 key_flux_meas: str | None = None,
1092 bands_color: dict[str, str] | None = None,
1093 ):
1094 if key_flux_meas is not None:
1095 _set_field_config(self, name="mag_sn", value=key_flux_meas)
1097 def setDefaults(self):
1098 super().setDefaults()
1099 self.produce.plot.yLims = self.limits_diff_pos_mas_default
1102class MatchedRefCoaddDiffPositionZoomTool(MatchedRefCoaddDiffPositionTool):
1103 def setDefaults(self):
1104 super().setDefaults()
1105 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default
1106 self.produce.metric = NoMetric
1109class MatchedRefCoaddDiffCoordRaTool(MatchedRefCoaddDiffPositionTool):
1110 def setDefaults(self):
1111 super().setDefaults()
1112 self.coord_meas = "coord_ra"
1113 self.coord_ref = "ref_ra"
1114 self.coord_ref_cos = "ref_dec"
1117class MatchedRefCoaddDiffCoordRaZoomTool(MatchedRefCoaddDiffCoordRaTool):
1118 def setDefaults(self):
1119 super().setDefaults()
1120 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default
1121 self.produce.metric = NoMetric
1124class MatchedRefCoaddChiCoordRaTool(MatchedRefCoaddDiffCoordRaTool):
1125 def setDefaults(self):
1126 super().setDefaults()
1127 self.compute_chi = True
1128 self.produce.plot.yLims = self.limits_chi_default
1131class MatchedRefCoaddDiffCoordDecTool(MatchedRefCoaddDiffPositionTool):
1132 def setDefaults(self):
1133 super().setDefaults()
1134 self.coord_meas = "coord_dec"
1135 self.coord_ref = "ref_dec"
1138class MatchedRefCoaddDiffCoordDecZoomTool(MatchedRefCoaddDiffCoordDecTool):
1139 def setDefaults(self):
1140 super().setDefaults()
1141 self.produce.plot.yLims = self.limits_diff_pos_mas_zoom_default
1142 self.produce.metric = NoMetric
1145class MatchedRefCoaddChiCoordDecTool(MatchedRefCoaddDiffCoordDecTool):
1146 def setDefaults(self):
1147 super().setDefaults()
1148 self.compute_chi = True
1149 self.produce.plot.yLims = self.limits_chi_default
1152class MatchedRefCoaddDiffDistanceTool(MatchedRefCoaddDiffPlot):
1153 """Tool for distances between matched reference and measured coadd
1154 objects."""
1156 key_dist = Field[str](default="match_distance", doc="Distance field key")
1157 key_dist_err = Field[str](default="match_distanceErr", doc="Distance error field key")
1158 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
1159 # Default coords are in degrees and we want mas
1160 scale_factor = Field[float](
1161 doc="The factor to multiply distances by (e.g. the pixel scale if coordinates have pixel units or "
1162 "the desired spherical coordinate unit conversion otherwise)",
1163 default=3600000,
1164 )
1166 def finalize(self):
1167 # Check if it has already been finalized
1168 if not hasattr(self.process.buildActions, "diff"): 1168 ↛ exitline 1168 didn't return from function 'finalize' because the condition on line 1168 was always true
1169 # Set before MagnitudeScatterPlot.finalize to undo PSF default.
1170 # Matched ref tables may not have PSF fluxes, or prefer CModel.
1171 self._set_flux_default("mag_sn")
1172 super().finalize()
1173 self._set_actions()
1175 name_short_x = self.config_mag_x.name_flux_short
1176 name_short_y = self.config_mag_y.name_flux_short
1178 self.process.buildActions.dist = LoadVector(vectorKey=self.key_dist)
1179 if self.compute_chi:
1180 self.process.buildActions.diff = DivideVector(
1181 actionA=self.process.buildActions.dist,
1182 actionB=LoadVector(vectorKey=self.key_dist_err),
1183 )
1184 else:
1185 self.process.buildActions.diff = MultiplyVector(
1186 actionA=ConstantValue(value=self.scale_factor),
1187 actionB=self.process.buildActions.dist,
1188 )
1189 if self.unit is None: 1189 ↛ 1191line 1189 didn't jump to line 1191 because the condition on line 1189 was always true
1190 self.unit = "" if self.compute_chi else "mas"
1191 if self.name_prefix is None: 1191 ↛ 1195line 1191 didn't jump to line 1195 because the condition on line 1191 was always true
1192 subtype = "chi" if self.compute_chi else "diff"
1193 self.name_prefix = f"astrom_dist_{{name_type}}_{subtype}_"
1194 self.name_prefix = f"astrom_{name_short_y}_vs_{name_short_x}_dist_{subtype}_{{name_type}}_"
1195 if self.do_metrics() and not self.produce.metric.units: 1195 ↛ 1197line 1195 didn't jump to line 1197 because the condition on line 1195 was always true
1196 self.produce.metric.units = self.configureMetrics()
1197 if not self.produce.plot.yAxisLabel: 1197 ↛ exitline 1197 didn't return from function 'finalize' because the condition on line 1197 was always true
1198 label = f"({name_short_y} - {name_short_x}) distance"
1199 self.produce.plot.yAxisLabel = (
1200 f"χ = ({label})/σ" if self.compute_chi else f"{label} ({self.unit})"
1201 )
1203 def get_key_flux_y(self) -> str:
1204 return self.mag_sn
1206 def reconfigure_dependent_magnitudes(
1207 self,
1208 key_flux_meas: str | None = None,
1209 bands_color: dict[str, str] | None = None,
1210 ):
1211 if key_flux_meas is not None:
1212 _set_field_config(self, name="mag_sn", value=key_flux_meas)
1214 def setDefaults(self):
1215 super().setDefaults()
1216 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_default[1]]
1219class MatchedRefCoaddChiDistanceTool(MatchedRefCoaddDiffDistanceTool):
1220 def setDefaults(self):
1221 super().setDefaults()
1222 self.compute_chi = True
1223 self.produce.plot.yLims = self.limits_chi_default
1226class MatchedRefCoaddDiffDistanceZoomTool(MatchedRefCoaddDiffDistanceTool):
1227 def setDefaults(self):
1228 super().setDefaults()
1229 self.produce.plot.yLims = [0, self.limits_diff_pos_mas_zoom_default[1]]
1230 self.produce.metric = NoMetric
1233def reconfigure_diff_matched_defaults(
1234 config: AnalysisBaseConfig | None = None,
1235 context: str | None = None,
1236 key_flux_meas: str | None = None,
1237 bands_color: dict[str, str] | list[str] | None = None,
1238 use_any: bool | None = None,
1239 use_galaxies: bool | None = None,
1240 use_stars: bool | None = None,
1241):
1242 """Reconfigure the default values for config fields of MatchedRefCoaddTool
1243 and all of its subclasses.
1245 Parameters
1246 ----------
1247 config
1248 An existing analysis config. Overrides will be applied to any of its
1249 member MatchedRefCoaddTool atools.
1250 context
1251 The context to set. Must be a valid choice for
1252 MatchedRefCoaddTool.context.
1253 key_flux_meas
1254 The key of the measured flux config to use, e.g. "psf". If the key is
1255 not found, it will search for f"{key}_err", the default name for
1256 configurations that load error keys as well as fluxes.
1257 bands_color
1258 A dictionary keyed by band of comma-separated bands to measure
1259 colors for, where the color is (key - value). If a list is passed,
1260 tools will modify the defaults to select only those bands within
1261 the list (which should also be a set).
1262 use_any
1263 Whether to compute metrics for objects of all types.
1264 use_galaxies
1265 Whether to compute metrics and plot lines for galaxies only.
1266 use_stars
1267 Whether to compute metrics and plot lines for stars only.
1269 Notes
1270 -----
1271 Any kwargs set to None will not change the relevant config field defaults.
1272 """
1273 if key_flux_meas is not None:
1274 keys_flux = tuple(MagnitudeTool.fluxes_default.toDict().keys())
1275 if key_flux_meas not in keys_flux:
1276 key_flux_err = f"{key_flux_meas}_err"
1277 if key_flux_err not in keys_flux:
1278 raise ValueError(
1279 f"{key_flux_meas=} and {key_flux_err} not found in available keys: {keys_flux}"
1280 f" (from MagnitudeTool.fluxes_default.toDict().keys())"
1281 )
1282 key_flux_meas = key_flux_err
1284 # These are class attributes and don't need to be changed in subclasses
1285 # These may end up being changed multiple times with repeated calls,
1286 # but there isn't a good way to avoid that.
1287 MagnitudeTool.fluxes_default.ref_matched.name_flux = "True"
1288 MagnitudeTool.fluxes_default.ref_matched.name_flux_short = "true"
1289 MagnitudeTool.fluxes_default.ref_matched.key_flux = "ref_{band}_flux"
1291 def all_subclasses(cls):
1292 return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
1294 subclasses = all_subclasses(MatchedRefCoaddTool)
1296 """
1297 Further context on these kwargs (get it?) and why they're not contexts:
1299 context (DC2, source_injection, etc)
1300 This could be made an AnalysisContext, but ChoiceField has the
1301 benefits of automatic validation. Also, subclasses refer to this config
1302 field without having to implement separate context functions.
1303 key_flux_meas
1304 This is the key to a FluxConfig. Default FluxConfigs could be mapped
1305 onto an AnalysisContext instead.
1306 bands_color
1307 This applies only to color tools and is intended to be set by
1308 obs package config overrides, e.g. to drop u-band colours. There is no
1309 way for obs packages to change Tool instance values and the bands
1310 config field is part of the PipelineTask and not accessible to tools,
1311 so no obvious alternative exists.
1312 use_*
1313 Like context, these apply to all subclasses, but are independent
1314 booleans rather than exclusive choices.
1315 """
1317 # This sets defaults for all known subclasses
1318 for tool in subclasses:
1319 tool.reconfigure(
1320 tool,
1321 context=context,
1322 key_flux_meas=key_flux_meas,
1323 bands_color=bands_color,
1324 use_any=use_any,
1325 use_galaxies=use_galaxies,
1326 use_stars=use_stars,
1327 )
1329 # This sets defaults for all existing tools
1330 # If a pipeline A imports a pipeline B, any atools already set in B will
1331 # be instantiated before overrides from A are applied. Therefore, changing
1332 # only the defaults will have no effect on those existing tools.
1333 if config is not None:
1334 for tool in config.atools:
1335 if isinstance(tool, MatchedRefCoaddTool):
1336 tool: MatchedRefCoaddTool = tool
1337 tool.reconfigure(
1338 context=context,
1339 key_flux_meas=key_flux_meas,
1340 bands_color=bands_color,
1341 use_any=use_any,
1342 use_galaxies=use_galaxies,
1343 use_stars=use_stars,
1344 )