Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 22%
98 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-29 11:33 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-29 11:33 +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 "MatchedRefCoaddDiffTool",
26 "MatchedRefCoaddDiffMagTool",
27 "MatchedRefCoaddDiffPositionTool",
28)
30from lsst.pex.config import Field
32from ..actions.vector import (
33 CalcBinnedStatsAction,
34 ConstantValue,
35 DivideVector,
36 DownselectVector,
37 LoadVector,
38 MultiplyVector,
39 SubtractVector,
40)
41from ..actions.vector.selectors import RangeSelector, VectorSelector
42from .genericBuild import ExtendednessTool, MagnitudeXTool
43from .genericProduce import MagnitudeScatterPlot
46class MatchedRefCoaddToolBase(MagnitudeXTool, ExtendednessTool):
47 """Base tool for matched-to-reference metrics/plots on coadds.
49 Metrics/plots are expected to use the reference magnitude and
50 require separate star/galaxy/all source selections.
52 Notes
53 -----
54 The tool does not use a standard coadd flag selector, because
55 it is expected that the matcher has been configured to select
56 appropriate candidates (and stores a match_candidate column).
57 """
59 def setDefaults(self):
60 super().setDefaults()
61 self.mag_x = "ref_matched"
63 def finalize(self):
64 super().finalize()
65 self._set_flux_default("mag_x")
68class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase):
69 """Base tool for generic diffs between reference and measured values."""
71 compute_chi = Field[bool](
72 default=False,
73 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
74 )
75 # These are optional because validate can be called before finalize
76 # Validate should not fail in that case if it would otherwise succeed
77 name_prefix = Field[str](doc="Prefix for metric key", default=None, optional=True)
78 unit = Field[str](doc="Astropy unit of y-axis values", default=None, optional=True)
80 _mag_low_min: int = 15
81 _mag_low_max: int = 27
82 _mag_interval: int = 1
84 _names = ("stars", "galaxies", "all")
85 _types = ("unresolved", "resolved", "all")
87 def configureMetrics(
88 self,
89 unit: str | None = None,
90 name_prefix: str | None = None,
91 name_suffix: str = "_ref_mag{minimum}",
92 unit_select: str = "mag",
93 ):
94 """Configure metric actions and return units.
96 Parameters
97 ----------
98 unit : `str`
99 The (astropy) unit of the summary statistic metrics.
100 name_prefix : `str`
101 The prefix for the action (column) name.
102 name_suffix : `str`
103 The sufffix for the action (column) name.
104 unit_select : `str`
105 The (astropy) unit of the selection (x-axis) column. Default "mag".
107 Returns
108 -------
109 units : `dict` [`str`, `str`]
110 A dict of the unit (value) for each metric name (key)
111 """
112 if unit is None:
113 unit = self.unit if self.unit is not None else ""
114 if name_prefix is None:
115 name_prefix = self.name_prefix if self.name_prefix is not None else ""
117 if unit_select is None:
118 unit_select = "mag"
120 key_flux = self.config_mag_x.key_flux
122 units = {}
123 for name, name_class in zip(self._names, self._types):
124 name_capital = name.capitalize()
125 x_key = f"x{name_capital}"
127 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
128 action = getattr(self.process.calculateActions, f"{name}{minimum}")
129 action.selector_range = RangeSelector(
130 vectorKey=x_key,
131 minimum=minimum,
132 maximum=minimum + self._mag_interval,
133 )
135 action.name_prefix = name_prefix.format(key_flux=key_flux, name_class=name_class)
136 if self.parameterizedBand:
137 action.name_prefix = f"{{band}}_{action.name_prefix}"
138 action.name_suffix = name_suffix.format(minimum=minimum)
140 units.update(
141 {
142 action.name_median: unit,
143 action.name_sigmaMad: unit,
144 action.name_count: "count",
145 action.name_select_minimum: unit_select,
146 action.name_select_median: unit_select,
147 action.name_select_maximum: unit_select,
148 }
149 )
150 return units
152 def setDefaults(self):
153 super().setDefaults()
155 self.process.filterActions.yAll = DownselectVector(
156 vectorKey="diff", selector=VectorSelector(vectorKey="allSelector")
157 )
158 self.process.filterActions.yGalaxies = DownselectVector(
159 vectorKey="diff", selector=VectorSelector(vectorKey="galaxySelector")
160 )
161 self.process.filterActions.yStars = DownselectVector(
162 vectorKey="diff", selector=VectorSelector(vectorKey="starSelector")
163 )
165 for name in self._names:
166 key = f"y{name.capitalize()}"
167 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
168 setattr(
169 self.process.calculateActions,
170 f"{name}{minimum}",
171 CalcBinnedStatsAction(
172 key_vector=key,
173 selector_range=RangeSelector(
174 vectorKey=key,
175 minimum=minimum,
176 maximum=minimum + self._mag_interval,
177 ),
178 ),
179 )
182class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
183 """Tool for diffs between reference and measured coadd mags."""
185 mag_y = Field[str](default="cmodel_err", doc="Flux (magnitude) field to difference against ref")
187 def finalize(self):
188 # Check if it has already been finalized
189 if not hasattr(self.process.buildActions, "diff"):
190 # Ensure mag_y is set before any plot finalizes
191 self._set_flux_default("mag_y")
192 super().finalize()
193 if self.compute_chi:
194 self.process.buildActions.diff = DivideVector(
195 actionA=SubtractVector(
196 actionA=getattr(self.process.buildActions, f"flux_{self.mag_y}"),
197 actionB=self.process.buildActions.flux_ref_matched,
198 ),
199 actionB=getattr(self.process.buildActions, f"flux_err_{self.mag_y}"),
200 )
201 else:
202 self.process.buildActions.diff = DivideVector(
203 actionA=SubtractVector(
204 actionA=getattr(self.process.buildActions, f"mag_{self.mag_y}"),
205 actionB=self.process.buildActions.mag_ref_matched,
206 ),
207 actionB=ConstantValue(value=1e-3),
208 )
209 if not self.produce.plot.yAxisLabel:
210 config = self.fluxes[self.mag_y]
211 label = f"{config.name_flux} - {self.fluxes['ref_matched'].name_flux}"
212 self.produce.plot.yAxisLabel = (
213 f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)"
214 )
215 if self.unit is None:
216 self.unit = "" if self.compute_chi else "mmag"
217 if self.name_prefix is None:
218 subtype = "chi" if self.compute_chi else "diff"
219 self.name_prefix = f"photom_mag_{{key_flux}}_{{name_class}}_{subtype}_"
220 if not self.produce.metric.units:
221 self.produce.metric.units = self.configureMetrics()
224class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
225 """Tool for diffs between reference and measured coadd astrometry."""
227 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
228 scale_factor = Field[float](
229 doc="The factor to multiply positions by (i.e. the pixel scale if coordinates have pixel units)",
230 default=200,
231 )
232 coord_label = Field[str](
233 doc="The plot label for the astrometric variable (default coord_meas)",
234 optional=True,
235 default=None,
236 )
237 coord_meas = Field[str](
238 doc="The key for measured values of the astrometric variable",
239 optional=False,
240 )
241 coord_ref = Field[str](
242 doc="The key for reference values of the astrometric variabler",
243 optional=False,
244 )
246 def finalize(self):
247 # Check if it has already been finalized
248 if not hasattr(self.process.buildActions, "diff"):
249 # Set before MagnitudeScatterPlot.finalize to undo PSF default.
250 # Matched ref tables may not have PSF fluxes, or prefer CModel.
251 self._set_flux_default("mag_sn")
252 super().finalize()
253 name = self.coord_label if self.coord_label else self.coord_meas
254 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.coord_meas)
255 self.process.buildActions.pos_ref = LoadVector(vectorKey=self.coord_ref)
256 if self.compute_chi:
257 self.process.buildActions.diff = DivideVector(
258 actionA=SubtractVector(
259 actionA=self.process.buildActions.pos_meas,
260 actionB=self.process.buildActions.pos_ref,
261 ),
262 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"),
263 )
264 else:
265 self.process.buildActions.diff = MultiplyVector(
266 actionA=ConstantValue(value=self.scale_factor),
267 actionB=SubtractVector(
268 actionA=self.process.buildActions.pos_meas,
269 actionB=self.process.buildActions.pos_ref,
270 ),
271 )
272 if self.unit is None:
273 self.unit = "" if self.compute_chi else "mas"
274 if self.name_prefix is None:
275 subtype = "chi" if self.compute_chi else "diff"
276 self.name_prefix = f"astrom_{self.coord_meas}_{{name_class}}_{subtype}_"
277 if not self.produce.metric.units:
278 self.produce.metric.units = self.configureMetrics()
279 if not self.produce.plot.yAxisLabel:
280 self.produce.plot.yAxisLabel = (
281 f"chi = (slot - reference {name} position)/error"
282 if self.compute_chi
283 else f"slot - reference {name} position ({self.unit})"
284 )