Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 18%
217 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 02:33 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 02:33 -0700
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 "ReferenceGalaxySelector",
25 "ReferenceObjectSelector",
26 "ReferenceStarSelector",
27 "MatchedRefCoaddToolBase",
28 "MatchedRefCoaddDiffTool",
29 "MatchedRefCoaddDiffColorTool",
30 "MatchedRefCoaddDiffMagTool",
31 "MatchedRefCoaddDiffPositionTool",
32)
34import copy
36from lsst.pex.config import DictField, Field
38from ..actions.vector import (
39 CalcBinnedStatsAction,
40 ColorDiff,
41 ColorError,
42 ConstantValue,
43 DivideVector,
44 DownselectVector,
45 LoadVector,
46 MultiplyVector,
47 SubtractVector,
48)
49from ..actions.vector.selectors import RangeSelector, ThresholdSelector, VectorSelector
50from ..interfaces import KeyedData, Vector
51from .genericBuild import MagnitudeXTool
52from .genericMetricAction import StructMetricAction
53from .genericPlotAction import StructPlotAction
54from .genericProduce import MagnitudeScatterPlot
57class ReferenceGalaxySelector(ThresholdSelector):
58 """A selector that selects galaxies from a catalog with a
59 boolean column identifying unresolved sources.
60 """
62 def __call__(self, data: KeyedData, **kwargs) -> Vector:
63 result = super().__call__(data=data, **kwargs)
64 if self.plotLabelKey:
65 self._addValueToPlotInfo("true galaxies", **kwargs)
66 return result
68 def setDefaults(self):
69 super().setDefaults()
70 self.op = "eq"
71 self.threshold = 0
72 self.plotLabelKey = "Selection: Galaxies"
73 self.vectorKey = "refcat_is_pointsource"
76class ReferenceObjectSelector(RangeSelector):
77 """A selector that selects all objects from a catalog with a
78 boolean column identifying unresolved sources.
79 """
81 def setDefaults(self):
82 super().setDefaults()
83 self.minimum = 0
84 self.vectorKey = "refcat_is_pointsource"
87class ReferenceStarSelector(ThresholdSelector):
88 """A selector that selects stars from a catalog with a
89 boolean column identifying unresolved sources.
90 """
92 def __call__(self, data: KeyedData, **kwargs) -> Vector:
93 result = super().__call__(data=data, **kwargs)
94 if self.plotLabelKey:
95 self._addValueToPlotInfo("true stars", **kwargs)
96 return result
98 def setDefaults(self):
99 super().setDefaults()
100 self.op = "eq"
101 self.plotLabelKey = "Selection: Stars"
102 self.threshold = 1
103 self.vectorKey = "refcat_is_pointsource"
106class MatchedRefCoaddToolBase(MagnitudeXTool):
107 """Base tool for matched-to-reference metrics/plots on coadds.
109 Metrics/plots are expected to use the reference magnitude and
110 require separate star/galaxy/all source selections.
112 Notes
113 -----
114 The tool does not use a standard coadd flag selector, because
115 it is expected that the matcher has been configured to select
116 appropriate candidates (and stores a match_candidate column).
117 """
119 def setDefaults(self):
120 super().setDefaults()
121 self.mag_x = "ref_matched"
122 self.process.buildActions.allSelector = ReferenceObjectSelector()
123 self.process.buildActions.galaxySelector = ReferenceGalaxySelector()
124 self.process.buildActions.starSelector = ReferenceStarSelector()
126 def finalize(self):
127 super().finalize()
128 self._set_flux_default("mag_x")
131class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase):
132 """Base tool for generic diffs between reference and measured values."""
134 compute_chi = Field[bool](
135 default=False,
136 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
137 )
138 # These are optional because validate can be called before finalize
139 # Validate should not fail in that case if it would otherwise succeed
140 name_prefix = Field[str](doc="Prefix for metric key", default=None, optional=True)
141 unit = Field[str](doc="Astropy unit of y-axis values", default=None, optional=True)
143 _mag_low_min: int = 15
144 _mag_low_max: int = 27
145 _mag_interval: int = 1
147 _names = {"stars": "star", "galaxies": "galaxy", "all": "all"}
148 _types = ("unresolved", "resolved", "all")
150 def _set_actions(self, suffix=None):
151 if suffix is None:
152 suffix = ""
154 for name_lower, name_singular in self._names.items():
155 name = name_lower.capitalize()
156 key = f"y{name}{suffix}"
157 setattr(
158 self.process.filterActions,
159 key,
160 DownselectVector(
161 vectorKey=f"diff{suffix}", selector=VectorSelector(vectorKey=f"{name_singular}Selector")
162 ),
163 )
165 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
166 setattr(
167 self.process.calculateActions,
168 f"{name_lower}{minimum}{suffix}",
169 CalcBinnedStatsAction(
170 key_vector=key,
171 selector_range=RangeSelector(
172 vectorKey=key,
173 minimum=minimum,
174 maximum=minimum + self._mag_interval,
175 ),
176 return_minmax=False,
177 ),
178 )
180 def configureMetrics(
181 self,
182 unit: str | None = None,
183 name_prefix: str | None = None,
184 name_suffix: str = "_ref_mag{minimum}",
185 attr_suffix: str | None = None,
186 unit_select: str = "mag",
187 ):
188 """Configure metric actions and return units.
190 Parameters
191 ----------
192 unit : `str`
193 The (astropy) unit of the summary statistic metrics.
194 name_prefix : `str`
195 The prefix for the action (column) name.
196 name_suffix : `str`
197 The suffix for the action (column) name.
198 attr_suffix : `str`
199 The suffix for the attribute to assign the action to.
200 unit_select : `str`
201 The (astropy) unit of the selection (x-axis) column. Default "mag".
203 Returns
204 -------
205 units : `dict` [`str`, `str`]
206 A dict of the unit (value) for each metric name (key)
207 """
208 if unit is None:
209 unit = self.unit if self.unit is not None else ""
210 if name_prefix is None:
211 name_prefix = self.name_prefix if self.name_prefix is not None else ""
212 if attr_suffix is None:
213 attr_suffix = ""
215 if unit_select is None:
216 unit_select = "mag"
218 key_flux = self.config_mag_x.key_flux
220 units = {}
221 for name, name_class in zip(self._names, self._types):
222 name_capital = name.capitalize()
223 x_key = f"x{name_capital}"
225 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
226 action = getattr(self.process.calculateActions, f"{name}{minimum}{attr_suffix}")
227 action.selector_range = RangeSelector(
228 vectorKey=x_key,
229 minimum=minimum,
230 maximum=minimum + self._mag_interval,
231 )
233 action.name_prefix = name_prefix.format(key_flux=key_flux, name_class=name_class)
234 if self.parameterizedBand:
235 action.name_prefix = f"{{band}}_{action.name_prefix}"
236 action.name_suffix = name_suffix.format(minimum=minimum)
238 units.update(
239 {
240 action.name_median: unit,
241 action.name_sigmaMad: unit,
242 action.name_count: "count",
243 action.name_select_median: unit_select,
244 }
245 )
246 return units
248 def setDefaults(self):
249 super().setDefaults()
250 self._set_actions()
253class MatchedRefCoaddDiffColorTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
254 """Tool for diffs between reference and measured coadd mags.
256 Notes
257 -----
258 Since this tool requires at least two bands, it is essentially impossible
259 to call on its own.
260 """
262 mag_y1 = Field[str](default="cmodel_err", doc="Flux field for first magnitude")
263 mag_y2 = Field[str](
264 doc="Flux field for second magnitude (to subtract from first); default same as first",
265 default=None,
266 optional=True,
267 )
268 bands = DictField[str, str](
269 doc="Bands for colors. ",
270 default={"u": "g", "g": "r,i", "r": "i", "i": "z", "z": "y"},
271 )
272 band_separator = Field[str](default=",", doc="Separator for multiple bands")
274 def _split_bands(self, band_list: str):
275 return band_list.split(self.band_separator)
277 def finalize(self):
278 # Check if it has already been finalized
279 if not hasattr(self.process.buildActions, "diff_0"):
280 if self.mag_y2 is None:
281 self.mag_y2 = self.mag_y1
282 # Ensure mag_y1/2 are set before any plot finalizes
283 bands = {band1: self._split_bands(band2_list) for band1, band2_list in self.bands.items()}
284 n_bands = 0
285 for band1, band2_list in bands.items():
286 for band2 in band2_list:
287 mag1 = f"y_{band1}"
288 mag2 = f"y_{band2}"
289 mag_ref1 = f"refcat_flux_{band1}"
290 mag_ref2 = f"refcat_flux_{band2}"
291 self._set_flux_default(mag1, band=band1, name_mag=self.mag_y1)
292 self._set_flux_default(mag2, band=band2, name_mag=self.mag_y2)
293 self._set_flux_default(mag_ref1, band=band1, name_mag="ref_matched")
294 self._set_flux_default(mag_ref2, band=band2, name_mag="ref_matched")
295 n_bands += 1
297 self.suffixes_y_finalize = [f"_{idx}" for idx in range(n_bands)]
298 super().finalize()
300 self.unit = "" if self.compute_chi else "mmag"
301 subtype = "chi" if self.compute_chi else "diff"
303 metric_base = self.produce.metric
304 plot_base = self.produce.plot
306 actions_metric = {}
307 actions_plot = {}
309 idx = 0
310 for band1, band2_list in bands.items():
311 for band2 in band2_list:
312 name_color = f"{band1}_minus_{band2}"
313 # Keep this index-based to simplify finalize
314 suffix_y = f"_{idx}"
315 self._set_actions(suffix=suffix_y)
316 metric = copy.copy(metric_base)
317 self.name_prefix = f"photom_mag_{{key_flux}}_color_{name_color}_{{name_class}}_{subtype}_"
318 metric.units = self.configureMetrics(attr_suffix=suffix_y)
319 plot = copy.copy(plot_base)
321 plot.suffix_y = suffix_y
322 plot.suffix_stat = suffix_y
324 mag1 = f"{self.mag_y1}_{band1}"
325 mag2 = f"{self.mag_y2}_{band2}"
326 mag_ref1 = f"ref_matched_{band1}"
327 mag_ref2 = f"ref_matched_{band2}"
329 diff = ColorDiff(
330 color1_flux1=getattr(self.process.buildActions, f"flux_{mag1}"),
331 color1_flux2=getattr(self.process.buildActions, f"flux_{mag2}"),
332 color2_flux1=getattr(self.process.buildActions, f"flux_{mag_ref1}"),
333 color2_flux2=getattr(self.process.buildActions, f"flux_{mag_ref2}"),
334 )
336 if self.compute_chi:
337 diff = DivideVector(
338 actionA=diff,
339 actionB=ColorError(
340 flux_err1=DivideVector(
341 actionA=getattr(self.process.buildActions, f"flux_err_{mag1}"),
342 actionB=getattr(self.process.buildActions, f"flux_{mag1}"),
343 ),
344 flux_err2=DivideVector(
345 actionA=getattr(self.process.buildActions, f"flux_err_{mag2}"),
346 actionB=getattr(self.process.buildActions, f"flux_{mag2}"),
347 ),
348 ),
349 )
350 setattr(self.process.buildActions, f"diff{plot.suffix_y}", diff)
352 label = f"({band1} - {band2}) (meas - ref)"
353 label = f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)"
354 plot.yAxisLabel = label
355 actions_metric[name_color] = metric
356 actions_plot[name_color] = plot
357 idx += 1
358 action_metric = StructMetricAction()
359 for name_action, action in actions_metric.items():
360 setattr(action_metric.actions, name_action, action)
361 self.produce.metric = action_metric
362 action_plot = StructPlotAction()
363 for name_action, action in actions_plot.items():
364 setattr(action_plot.actions, name_action, action)
365 self.produce.plot = action_plot
367 def setDefaults(self):
368 # skip MatchedRefCoaddDiffTool.setDefaults's _setActions call
369 MatchedRefCoaddToolBase.setDefaults(self)
370 MagnitudeScatterPlot.setDefaults(self)
372 def validate(self):
373 super().validate()
374 errors = []
375 for band1, band2_list in self.bands.items():
376 bands = self._split_bands(band2_list)
377 if len(set(bands)) != len(bands):
378 errors.append(f"value={band2_list} is not a set for key={band1}")
379 if errors:
380 raise ValueError("\n".join(errors))
383class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
384 """Tool for diffs between reference and measured coadd mags."""
386 mag_y = Field[str](default="cmodel_err", doc="Flux (magnitude) field to difference against ref")
388 def finalize(self):
389 # Check if it has already been finalized
390 if not hasattr(self.process.buildActions, "diff"):
391 # Ensure mag_y is set before any plot finalizes
392 self._set_flux_default("mag_y")
393 super().finalize()
394 if self.compute_chi:
395 self.process.buildActions.diff = DivideVector(
396 actionA=SubtractVector(
397 actionA=getattr(self.process.buildActions, f"flux_{self.mag_y}"),
398 actionB=self.process.buildActions.flux_ref_matched,
399 ),
400 actionB=getattr(self.process.buildActions, f"flux_err_{self.mag_y}"),
401 )
402 else:
403 self.process.buildActions.diff = DivideVector(
404 actionA=SubtractVector(
405 actionA=getattr(self.process.buildActions, f"mag_{self.mag_y}"),
406 actionB=self.process.buildActions.mag_ref_matched,
407 ),
408 actionB=ConstantValue(value=1e-3),
409 )
410 if not self.produce.plot.yAxisLabel:
411 config = self.fluxes[self.mag_y]
412 label = f"{config.name_flux} - {self.fluxes['ref_matched'].name_flux}"
413 self.produce.plot.yAxisLabel = (
414 f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)"
415 )
416 if self.unit is None:
417 self.unit = "" if self.compute_chi else "mmag"
418 if self.name_prefix is None:
419 subtype = "chi" if self.compute_chi else "diff"
420 self.name_prefix = f"photom_mag_{{key_flux}}_{{name_class}}_{subtype}_"
421 if not self.produce.metric.units:
422 self.produce.metric.units = self.configureMetrics()
425class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
426 """Tool for diffs between reference and measured coadd astrometry."""
428 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
429 scale_factor = Field[float](
430 doc="The factor to multiply positions by (i.e. the pixel scale if coordinates have pixel units)",
431 default=200,
432 )
433 coord_label = Field[str](
434 doc="The plot label for the astrometric variable (default coord_meas)",
435 optional=True,
436 default=None,
437 )
438 coord_meas = Field[str](
439 doc="The key for measured values of the astrometric variable",
440 optional=False,
441 )
442 coord_ref = Field[str](
443 doc="The key for reference values of the astrometric variabler",
444 optional=False,
445 )
447 def finalize(self):
448 # Check if it has already been finalized
449 if not hasattr(self.process.buildActions, "diff"):
450 # Set before MagnitudeScatterPlot.finalize to undo PSF default.
451 # Matched ref tables may not have PSF fluxes, or prefer CModel.
452 self._set_flux_default("mag_sn")
453 super().finalize()
454 name = self.coord_label if self.coord_label else self.coord_meas
455 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.coord_meas)
456 self.process.buildActions.pos_ref = LoadVector(vectorKey=self.coord_ref)
457 if self.compute_chi:
458 self.process.buildActions.diff = DivideVector(
459 actionA=SubtractVector(
460 actionA=self.process.buildActions.pos_meas,
461 actionB=self.process.buildActions.pos_ref,
462 ),
463 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"),
464 )
465 else:
466 self.process.buildActions.diff = MultiplyVector(
467 actionA=ConstantValue(value=self.scale_factor),
468 actionB=SubtractVector(
469 actionA=self.process.buildActions.pos_meas,
470 actionB=self.process.buildActions.pos_ref,
471 ),
472 )
473 if self.unit is None:
474 self.unit = "" if self.compute_chi else "mas"
475 if self.name_prefix is None:
476 subtype = "chi" if self.compute_chi else "diff"
477 self.name_prefix = f"astrom_{self.coord_meas}_{{name_class}}_{subtype}_"
478 if not self.produce.metric.units:
479 self.produce.metric.units = self.configureMetrics()
480 if not self.produce.plot.yAxisLabel:
481 self.produce.plot.yAxisLabel = (
482 f"chi = (slot - reference {name} position)/error"
483 if self.compute_chi
484 else f"slot - reference {name} position ({self.unit})"
485 )