Coverage for python/lsst/analysis/tools/atools/diffMatched.py: 23%
131 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 12:21 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 12:21 +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 "ReferenceGalaxySelector",
25 "ReferenceObjectSelector",
26 "ReferenceStarSelector",
27 "MatchedRefCoaddToolBase",
28 "MatchedRefCoaddDiffTool",
29 "MatchedRefCoaddDiffMagTool",
30 "MatchedRefCoaddDiffPositionTool",
31)
33from lsst.pex.config import Field
35from ..actions.vector import (
36 CalcBinnedStatsAction,
37 ConstantValue,
38 DivideVector,
39 DownselectVector,
40 LoadVector,
41 MultiplyVector,
42 SubtractVector,
43)
44from ..actions.vector.selectors import RangeSelector, ThresholdSelector, VectorSelector
45from ..interfaces import KeyedData, Vector
46from .genericBuild import MagnitudeXTool
47from .genericProduce import MagnitudeScatterPlot
50class ReferenceGalaxySelector(ThresholdSelector):
51 """A selector that selects galaxies from a catalog with a
52 boolean column identifying unresolved sources.
53 """
55 def __call__(self, data: KeyedData, **kwargs) -> Vector:
56 result = super().__call__(data=data, **kwargs)
57 if self.plotLabelKey:
58 self._addValueToPlotInfo("true galaxies", **kwargs)
59 return result
61 def setDefaults(self):
62 super().setDefaults()
63 self.op = "eq"
64 self.threshold = 0
65 self.plotLabelKey = "Selection: Galaxies"
66 self.vectorKey = "refcat_is_pointsource"
69class ReferenceObjectSelector(RangeSelector):
70 """A selector that selects all objects from a catalog with a
71 boolean column identifying unresolved sources.
72 """
74 def setDefaults(self):
75 super().setDefaults()
76 self.minimum = 0
77 self.vectorKey = "refcat_is_pointsource"
80class ReferenceStarSelector(ThresholdSelector):
81 """A selector that selects stars from a catalog with a
82 boolean column identifying unresolved sources.
83 """
85 def __call__(self, data: KeyedData, **kwargs) -> Vector:
86 result = super().__call__(data=data, **kwargs)
87 if self.plotLabelKey:
88 self._addValueToPlotInfo("true stars", **kwargs)
89 return result
91 def setDefaults(self):
92 super().setDefaults()
93 self.op = "eq"
94 self.plotLabelKey = "Selection: Stars"
95 self.threshold = 1
96 self.vectorKey = "refcat_is_pointsource"
99class MatchedRefCoaddToolBase(MagnitudeXTool):
100 """Base tool for matched-to-reference metrics/plots on coadds.
102 Metrics/plots are expected to use the reference magnitude and
103 require separate star/galaxy/all source selections.
105 Notes
106 -----
107 The tool does not use a standard coadd flag selector, because
108 it is expected that the matcher has been configured to select
109 appropriate candidates (and stores a match_candidate column).
110 """
112 def setDefaults(self):
113 super().setDefaults()
114 self.mag_x = "ref_matched"
115 self.process.buildActions.allSelector = ReferenceObjectSelector()
116 self.process.buildActions.galaxySelector = ReferenceGalaxySelector()
117 self.process.buildActions.starSelector = ReferenceStarSelector()
119 def finalize(self):
120 super().finalize()
121 self._set_flux_default("mag_x")
124class MatchedRefCoaddDiffTool(MatchedRefCoaddToolBase):
125 """Base tool for generic diffs between reference and measured values."""
127 compute_chi = Field[bool](
128 default=False,
129 doc="Whether to compute scaled flux residuals (chi) instead of magnitude differences",
130 )
131 # These are optional because validate can be called before finalize
132 # Validate should not fail in that case if it would otherwise succeed
133 name_prefix = Field[str](doc="Prefix for metric key", default=None, optional=True)
134 unit = Field[str](doc="Astropy unit of y-axis values", default=None, optional=True)
136 _mag_low_min: int = 15
137 _mag_low_max: int = 27
138 _mag_interval: int = 1
140 _names = ("stars", "galaxies", "all")
141 _types = ("unresolved", "resolved", "all")
143 def configureMetrics(
144 self,
145 unit: str | None = None,
146 name_prefix: str | None = None,
147 name_suffix: str = "_ref_mag{minimum}",
148 unit_select: str = "mag",
149 ):
150 """Configure metric actions and return units.
152 Parameters
153 ----------
154 unit : `str`
155 The (astropy) unit of the summary statistic metrics.
156 name_prefix : `str`
157 The prefix for the action (column) name.
158 name_suffix : `str`
159 The sufffix for the action (column) name.
160 unit_select : `str`
161 The (astropy) unit of the selection (x-axis) column. Default "mag".
163 Returns
164 -------
165 units : `dict` [`str`, `str`]
166 A dict of the unit (value) for each metric name (key)
167 """
168 if unit is None:
169 unit = self.unit if self.unit is not None else ""
170 if name_prefix is None:
171 name_prefix = self.name_prefix if self.name_prefix is not None else ""
173 if unit_select is None:
174 unit_select = "mag"
176 key_flux = self.config_mag_x.key_flux
178 units = {}
179 for name, name_class in zip(self._names, self._types):
180 name_capital = name.capitalize()
181 x_key = f"x{name_capital}"
183 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
184 action = getattr(self.process.calculateActions, f"{name}{minimum}")
185 action.selector_range = RangeSelector(
186 vectorKey=x_key,
187 minimum=minimum,
188 maximum=minimum + self._mag_interval,
189 )
191 action.name_prefix = name_prefix.format(key_flux=key_flux, name_class=name_class)
192 if self.parameterizedBand:
193 action.name_prefix = f"{{band}}_{action.name_prefix}"
194 action.name_suffix = name_suffix.format(minimum=minimum)
196 units.update(
197 {
198 action.name_median: unit,
199 action.name_sigmaMad: unit,
200 action.name_count: "count",
201 action.name_select_minimum: unit_select,
202 action.name_select_median: unit_select,
203 action.name_select_maximum: unit_select,
204 }
205 )
206 return units
208 def setDefaults(self):
209 super().setDefaults()
211 self.process.filterActions.yAll = DownselectVector(
212 vectorKey="diff", selector=VectorSelector(vectorKey="allSelector")
213 )
214 self.process.filterActions.yGalaxies = DownselectVector(
215 vectorKey="diff", selector=VectorSelector(vectorKey="galaxySelector")
216 )
217 self.process.filterActions.yStars = DownselectVector(
218 vectorKey="diff", selector=VectorSelector(vectorKey="starSelector")
219 )
221 for name in self._names:
222 key = f"y{name.capitalize()}"
223 for minimum in range(self._mag_low_min, self._mag_low_max + 1):
224 setattr(
225 self.process.calculateActions,
226 f"{name}{minimum}",
227 CalcBinnedStatsAction(
228 key_vector=key,
229 selector_range=RangeSelector(
230 vectorKey=key,
231 minimum=minimum,
232 maximum=minimum + self._mag_interval,
233 ),
234 ),
235 )
238class MatchedRefCoaddDiffMagTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
239 """Tool for diffs between reference and measured coadd mags."""
241 mag_y = Field[str](default="cmodel_err", doc="Flux (magnitude) field to difference against ref")
243 def finalize(self):
244 # Check if it has already been finalized
245 if not hasattr(self.process.buildActions, "diff"):
246 # Ensure mag_y is set before any plot finalizes
247 self._set_flux_default("mag_y")
248 super().finalize()
249 if self.compute_chi:
250 self.process.buildActions.diff = DivideVector(
251 actionA=SubtractVector(
252 actionA=getattr(self.process.buildActions, f"flux_{self.mag_y}"),
253 actionB=self.process.buildActions.flux_ref_matched,
254 ),
255 actionB=getattr(self.process.buildActions, f"flux_err_{self.mag_y}"),
256 )
257 else:
258 self.process.buildActions.diff = DivideVector(
259 actionA=SubtractVector(
260 actionA=getattr(self.process.buildActions, f"mag_{self.mag_y}"),
261 actionB=self.process.buildActions.mag_ref_matched,
262 ),
263 actionB=ConstantValue(value=1e-3),
264 )
265 if not self.produce.plot.yAxisLabel:
266 config = self.fluxes[self.mag_y]
267 label = f"{config.name_flux} - {self.fluxes['ref_matched'].name_flux}"
268 self.produce.plot.yAxisLabel = (
269 f"chi = ({label})/error" if self.compute_chi else f"{label} (mmag)"
270 )
271 if self.unit is None:
272 self.unit = "" if self.compute_chi else "mmag"
273 if self.name_prefix is None:
274 subtype = "chi" if self.compute_chi else "diff"
275 self.name_prefix = f"photom_mag_{{key_flux}}_{{name_class}}_{subtype}_"
276 if not self.produce.metric.units:
277 self.produce.metric.units = self.configureMetrics()
280class MatchedRefCoaddDiffPositionTool(MatchedRefCoaddDiffTool, MagnitudeScatterPlot):
281 """Tool for diffs between reference and measured coadd astrometry."""
283 mag_sn = Field[str](default="cmodel_err", doc="Flux (magnitude) field to use for S/N binning on plot")
284 scale_factor = Field[float](
285 doc="The factor to multiply positions by (i.e. the pixel scale if coordinates have pixel units)",
286 default=200,
287 )
288 coord_label = Field[str](
289 doc="The plot label for the astrometric variable (default coord_meas)",
290 optional=True,
291 default=None,
292 )
293 coord_meas = Field[str](
294 doc="The key for measured values of the astrometric variable",
295 optional=False,
296 )
297 coord_ref = Field[str](
298 doc="The key for reference values of the astrometric variabler",
299 optional=False,
300 )
302 def finalize(self):
303 # Check if it has already been finalized
304 if not hasattr(self.process.buildActions, "diff"):
305 # Set before MagnitudeScatterPlot.finalize to undo PSF default.
306 # Matched ref tables may not have PSF fluxes, or prefer CModel.
307 self._set_flux_default("mag_sn")
308 super().finalize()
309 name = self.coord_label if self.coord_label else self.coord_meas
310 self.process.buildActions.pos_meas = LoadVector(vectorKey=self.coord_meas)
311 self.process.buildActions.pos_ref = LoadVector(vectorKey=self.coord_ref)
312 if self.compute_chi:
313 self.process.buildActions.diff = DivideVector(
314 actionA=SubtractVector(
315 actionA=self.process.buildActions.pos_meas,
316 actionB=self.process.buildActions.pos_ref,
317 ),
318 actionB=LoadVector(vectorKey=f"{self.process.buildActions.pos_meas.vectorKey}Err"),
319 )
320 else:
321 self.process.buildActions.diff = MultiplyVector(
322 actionA=ConstantValue(value=self.scale_factor),
323 actionB=SubtractVector(
324 actionA=self.process.buildActions.pos_meas,
325 actionB=self.process.buildActions.pos_ref,
326 ),
327 )
328 if self.unit is None:
329 self.unit = "" if self.compute_chi else "mas"
330 if self.name_prefix is None:
331 subtype = "chi" if self.compute_chi else "diff"
332 self.name_prefix = f"astrom_{self.coord_meas}_{{name_class}}_{subtype}_"
333 if not self.produce.metric.units:
334 self.produce.metric.units = self.configureMetrics()
335 if not self.produce.plot.yAxisLabel:
336 self.produce.plot.yAxisLabel = (
337 f"chi = (slot - reference {name} position)/error"
338 if self.compute_chi
339 else f"slot - reference {name} position ({self.unit})"
340 )