Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 26%
111 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 18:06 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 18:06 +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 "MatchedRefCoaddToolBase",
25 "MatchedRefCoaddDiffMetric",
26 "MatchedRefCoaddDiffMagTool",
27 "MatchedRefCoaddDiffMagMetric",
28 "MatchedRefCoaddDiffPositionTool",
29 "MatchedRefCoaddDiffPositionMetric",
30)
32from lsst.pex.config import ChoiceField, Field
34from ..actions.vector import (
35 CalcBinnedStatsAction,
36 ConstantValue,
37 DivideVector,
38 DownselectVector,
39 LoadVector,
40 MultiplyVector,
41 SubtractVector,
42)
43from ..actions.vector.selectors import RangeSelector, VectorSelector
44from .genericBuild import ExtendednessTool, MagnitudeXTool
45from .genericProduce import MagnitudeScatterPlot
48class MatchedRefCoaddToolBase(MagnitudeXTool, ExtendednessTool):
49 """Base tool for matched-to-reference metrics/plots on coadds.
51 Metrics/plots are expected to use the reference magnitude and
52 require separate star/galaxy/all source selections.
54 Notes
55 -----
56 The tool does not use a standard coadd flag selector, because
57 it is expected that the matcher has been configured to select
58 appropriate candidates (and stores a match_candidate column).
59 """
61 def setDefaults(self):
62 super().setDefaults()
63 self.mag_x = "ref_matched"
65 def finalize(self):
66 super().finalize()
67 self._set_flux_default("mag_x")
70class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase):
71 """Base tool for generic diffs between reference and measured values."""
73 compute_chi = Field[bool](
74 default=False,
75 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
76 )
78 def setDefaults(self):
79 super().setDefaults()
80 self.process.filterActions.yAll = DownselectVector(
81 vectorKey="diff", selector=VectorSelector(vectorKey="allSelector")
82 )
83 self.process.filterActions.yGalaxies = DownselectVector(
84 vectorKey="diff", selector=VectorSelector(vectorKey="galaxySelector")
85 )
86 self.process.filterActions.yStars = DownselectVector(
87 vectorKey="diff", selector=VectorSelector(vectorKey="starSelector")
88 )
91class MatchedRefCoaddDiffMetric(MatchedRefCoaddDiffTool):
92 """Base tool for matched-to-reference metrics on coadds."""
94 # These are optional because validate can be called before finalize
95 # Validate should not fail in that case if it would otherwise succeed
96 name_prefix = Field[str](doc="Prefix for metric key", default=None, optional=True)
97 unit = Field[str](doc="Astropy unit of y-axis values", default=None, optional=True)
99 _mag_low_min: int = 15
100 _mag_low_max: int = 27
101 _mag_interval: int = 1
103 _names = ("stars", "galaxies", "all")
104 _types = ("unresolved", "resolved", "all")
106 def configureMetrics(
107 self,
108 unit: str | None = None,
109 name_prefix: str | None = None,
110 name_suffix: str = "_ref_mag{minimum}",
111 unit_select: str = "mag",
112 ):
113 """Configure metric actions and return units.
115 Parameters
116 ----------
117 unit : `str`
118 The (astropy) unit of the summary statistic metrics.
119 name_prefix : `str`
120 The prefix for the action (column) name.
121 name_suffix : `str`
122 The sufffix for the action (column) name.
123 unit_select : `str`
124 The (astropy) unit of the selection (x-axis) column. Default "mag".
126 Returns
127 -------
128 units : `dict` [`str`, `str`]
129 A dict of the unit (value) for each metric name (key)
130 """
131 if unit is None:
132 unit = self.unit if self.unit is not None else ""
133 if name_prefix is None:
134 name_prefix = self.name_prefix if self.name_prefix is not None else ""
136 if unit_select is None:
137 unit_select = "mag"
139 key_flux = self.config_mag_x.key_flux
141 units = {}
142 for name, name_class in zip(self._names, self._types):
143 name_capital = name.capitalize()
144 x_key = f"x{name_capital}"
146 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
147 action = getattr(self.process.calculateActions, f"{name}{minimum}")
148 action.selector_range = RangeSelector(
149 vectorKey=x_key,
150 minimum=minimum,
151 maximum=minimum + self._mag_interval,
152 )
154 action.name_prefix = name_prefix.format(key_flux=key_flux, name_class=name_class)
155 if self.parameterizedBand:
156 action.name_prefix = f"{{band}}_{action.name_prefix}"
157 action.name_suffix = name_suffix.format(minimum=minimum)
159 units.update(
160 {
161 action.name_median: unit,
162 action.name_sigmaMad: unit,
163 action.name_count: "count",
164 action.name_select_minimum: unit_select,
165 action.name_select_median: unit_select,
166 action.name_select_maximum: unit_select,
167 }
168 )
169 return units
171 def setDefaults(self):
172 super().setDefaults()
174 for name in self._names:
175 key = f"y{name.capitalize()}"
176 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
177 setattr(
178 self.process.calculateActions,
179 f"{name}{minimum}",
180 CalcBinnedStatsAction(
181 key_vector=key,
182 selector_range=RangeSelector(
183 vectorKey=key,
184 minimum=minimum,
185 maximum=minimum + self._mag_interval,
186 ),
187 ),
188 )
191class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool):
192 """Tool for diffs between reference and measured coadd mags."""
194 mag_y = Field[str](default="cmodel_err", doc="Flux (magnitude) field to difference against ref")
196 def finalize(self):
197 # Check if it has already been finalized
198 if not hasattr(self.process.buildActions, "diff"):
199 # TODO: Is this hack to ensure mag_y is set before plot tools
200 # are called necessary?
201 self._set_flux_default("mag_y")
202 super().finalize()
203 if self.compute_chi:
204 self.process.buildActions.diff = DivideVector(
205 actionA=SubtractVector(
206 actionA=getattr(self.process.buildActions, f"flux_{self.mag_y}"),
207 actionB=self.process.buildActions.flux_ref_matched,
208 ),
209 actionB=getattr(self.process.buildActions, f"flux_err_{self.mag_y}"),
210 )
211 else:
212 self.process.buildActions.diff = DivideVector(
213 actionA=SubtractVector(
214 actionA=getattr(self.process.buildActions, f"mag_{self.mag_y}"),
215 actionB=self.process.buildActions.mag_ref_matched,
216 ),
217 actionB=ConstantValue(value=1e-3),
218 )
221# The diamond inheritance on MatchedRefCoaddTool seems ok
222class MatchedRefCoaddDiffMagMetric(MatchedRefCoaddDiffMagTool, MatchedRefCoaddDiffMetric):
223 """Metric for diffs between reference and measured coadd mags."""
225 def finalize(self):
226 super().finalize()
227 if self.unit is None:
228 self.unit = "" if self.compute_chi else "mmag"
229 if self.name_prefix is None:
230 subtype = "chi" if self.compute_chi else "diff"
231 self.name_prefix = f"photom_mag_{{key_flux}}_{{name_class}}_{subtype}_"
232 if not self.produce.metric.units:
233 self.produce.metric.units = self.configureMetrics()
236class MatchedRefCoaddDiffMagPlot(MatchedRefCoaddDiffMagTool, MagnitudeScatterPlot):
237 def finalize(self):
238 # TODO: Check if this is really necessary
239 # finalizing in this order should get all fluxes finalized before
240 # the MagnitudeScatterPlot looks for a flux to compute S/N from
241 MatchedRefCoaddDiffMagTool.finalize(self)
242 MagnitudeScatterPlot.finalize(self)
243 if not self.produce.yAxisLabel:
244 config = self.fluxes[self.mag_y]
245 label = f"{config.name_flux} - {self.fluxes['ref_matched'].name_flux}"
246 self.produce.yAxisLabel = f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)"
249class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool):
250 """Tool for diffs between reference and measured coadd astrometry."""
252 scale_factor = Field[float](
253 doc="The factor to multiply positions by (i.e. the pixel scale if coordinates have pixel units)",
254 default=200,
255 )
256 variable = ChoiceField[str](
257 doc="The astrometric variable to compute metrics for",
258 allowed={
259 "x": "x",
260 "y": "y",
261 },
262 optional=False,
263 )
265 def finalize(self):
266 # Check if it has already been finalized
267 if not hasattr(self.process.buildActions, "diff"):
268 super().finalize()
269 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.variable)
270 self.process.buildActions.pos_ref = LoadVector(vectorKey=f"refcat_{self.variable}")
271 if self.compute_chi:
272 self.process.buildActions.diff = DivideVector(
273 actionA=SubtractVector(
274 actionA=self.process.buildActions.pos_meas,
275 actionB=self.process.buildActions.pos_ref,
276 ),
277 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"),
278 )
279 else:
280 self.process.buildActions.diff = MultiplyVector(
281 actionA=ConstantValue(value=self.scale_factor),
282 actionB=SubtractVector(
283 actionA=self.process.buildActions.pos_meas,
284 actionB=self.process.buildActions.pos_ref,
285 ),
286 )
289class MatchedRefCoaddDiffPositionMetric(MatchedRefCoaddDiffPositionTool, MatchedRefCoaddDiffMetric):
290 """Metric for diffs between reference and base coadd centroids."""
292 def finalize(self):
293 super().finalize()
294 if self.unit is None:
295 self.unit = "" if self.compute_chi else "mas"
296 if self.name_prefix is None:
297 subtype = "chi" if self.compute_chi else "diff"
298 self.name_prefix = f"astrom_{self.variable}_{{name_class}}_{subtype}_"
299 if not self.produce.metric.units:
300 self.produce.metric.units = self.configureMetrics()
303class MatchedRefCoaddDiffPositionPlot(MatchedRefCoaddDiffPositionTool, MagnitudeScatterPlot):
304 # The matched catalog columns are configurable but default to cmodel only
305 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
307 def finalize(self):
308 if not self.produce.yAxisLabel:
309 # Set before MagnitudeScatterPlot.finalize or it'll default to PSF
310 # Matched ref tables may not have PSF fluxes, or prefer CModel
311 self._set_flux_default("mag_sn")
312 super().finalize()
313 self.produce.yAxisLabel = (
314 f"chi = (slot - Reference {self.variable} position)/error"
315 if self.compute_chi
316 else f"{self.variable} position (pix)"
317 )