Coverage for python/lsst/analysis/tools/atools/genericBuild.py: 31%
162 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-23 04:51 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-23 04:51 -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__ = ("ExtendednessTool", "FluxConfig", "MagnitudeTool", "MagnitudeXTool", "SizeConfig", "SizeTool")
25import copy
27from lsst.pex.config import ChoiceField, Config, ConfigDictField, ConfigField, Field
28from lsst.pex.config.configurableActions import ConfigurableActionStructField
30from ..actions.vector import (
31 CalcMomentSize,
32 ConstantValue,
33 ConvertFluxToMag,
34 DownselectVector,
35 LoadVector,
36 Log10Vector,
37 MultiplyVector,
38 VectorSelector,
39)
40from ..actions.vector.selectors import (
41 CoaddPlotFlagSelector,
42 GalaxySelector,
43 StarSelector,
44 VisitPlotFlagSelector,
45)
46from ..interfaces import AnalysisTool, VectorAction
49class ExtendednessTool(AnalysisTool):
50 """Select (non-)extended sources in visit/coadd contexts."""
52 extendedness = Field[str](
53 default="refExtendedness",
54 doc="Extendedness field to select sub-samples with",
55 )
57 parameterizedBand = Field[bool](
58 default=True,
59 doc="Does this AnalysisTool support band as a name parameter",
60 )
62 def coaddContext(self) -> None:
63 self.selectors.flagSelector = CoaddPlotFlagSelector()
64 self.selectors.flagSelector.bands = ["{band}"]
66 def visitContext(self) -> None:
67 self.parameterizedBand = False
68 self.selectors.flagSelector = VisitPlotFlagSelector()
70 def setDefaults(self):
71 super().setDefaults()
72 # Select any finite extendedness (but still exclude NaNs)
73 self.process.buildActions.allSelector = StarSelector(
74 vectorKey=self.extendedness, extendedness_maximum=1.0
75 )
76 self.process.buildActions.galaxySelector = GalaxySelector(vectorKey=self.extendedness)
77 self.process.buildActions.starSelector = StarSelector(vectorKey=self.extendedness)
80class FluxConfig(Config):
81 """Configuration for a flux vector to be loaded and potentially plotted."""
83 band_format = Field[str](default="{band}_{key}", doc="Format of band-dependent flux keys.")
84 key_flux = Field[str](default=None, doc="Flux field to convert to magnitude on x-axis.")
85 key_flux_error = Field[str](default="{key_flux}Err", doc="Flux error field.", optional=True)
86 name_flux = Field[str](default=None, doc="Name of the flux/magnitude algorithm/model.")
88 def key_flux_band(self, band: str):
89 return self.band_format.format(band=band, key=self.key_flux)
91 def key_flux_error_band(self, band: str):
92 return self.band_format.format(band=band, key=self.key_flux_error.format(key_flux=self.key_flux))
95class FluxesDefaultConfig(Config):
96 bulge_err = ConfigField[FluxConfig](doc="Bulge model magnitude with errors")
97 cmodel_err = ConfigField[FluxConfig](doc="CModel total magnitude with errors")
98 disk_err = ConfigField[FluxConfig](doc="Disk model magnitude with errors")
99 psf_err = ConfigField[FluxConfig](doc="PSF model magnitude with errors")
100 ref_matched = ConfigField[FluxConfig](doc="Reference catalog magnitude")
103class MagnitudeTool(AnalysisTool):
104 """Compute magnitudes from flux columns.
106 Any tool that reads in flux columns and converts them to magnitudes can
107 derive from this class and use the _add_flux method to set the
108 necessary build actions in their own finalize() methods.
109 """
111 fluxes_default = FluxesDefaultConfig(
112 bulge_err=FluxConfig(key_flux="bdFluxB", name_flux="Bulge"),
113 cmodel_err=FluxConfig(key_flux="cModelFlux", name_flux="CModel"),
114 disk_err=FluxConfig(key_flux="bdFluxD", name_flux="Disk"),
115 psf_err=FluxConfig(key_flux="psfFlux", name_flux="PSF"),
116 ref_matched=FluxConfig(
117 key_flux="refcat_flux", name_flux="Reference", key_flux_error=None, band_format="{key}_{band}"
118 ),
119 )
121 fluxes = ConfigDictField[str, FluxConfig]( # type: ignore
122 default={},
123 doc="Flux fields to convert to magnitudes",
124 )
126 def _add_flux(self, name: str, config: FluxConfig):
127 key_flux = config.key_flux_band(band="{band}")
128 name_flux = f"flux_{name}"
129 self._set_action(self.process.buildActions, name_flux, LoadVector, vectorKey=key_flux)
130 if config.key_flux_error is not None:
131 # Pre-emptively loaded for e.g. future S/N calculations
132 key_flux_err = config.key_flux_error_band(band="{band}")
133 self._set_action(
134 self.process.buildActions, f"flux_err_{name}", LoadVector, vectorKey=key_flux_err
135 )
136 self._set_action(self.process.buildActions, f"mag_{name}", ConvertFluxToMag, vectorKey=key_flux)
138 def _set_action(self, target: ConfigurableActionStructField, name: str, action, *args, **kwargs):
139 """Set an action attribute on a target tool's struct field.
141 Parameters
142 ----------
143 target
144 The ConfigurableActionStructField to set an attribute on.
145 name
146 The name of the attribute to set.
147 action
148 The action class to set the attribute to.
149 args
150 Arguments to pass when initialization the action.
151 kwargs
152 Keyword arguments to pass when initialization the action.
153 """
154 if hasattr(target, name):
155 attr = getattr(target, name)
156 # Setting an attr to a different action is a logic error
157 assert isinstance(attr, action)
158 # Assert that the action's attributes are identical
159 for key, value in kwargs.items():
160 if value.__class__.__module__ == "__builtin__":
161 assert getattr(attr, key) == value
162 else:
163 setattr(target, name, action(*args, **kwargs))
165 def _set_flux_default(self, attr):
166 """Set own config attr to appropriate string flux name."""
167 name_mag = getattr(self, attr)
168 # Do nothing if already set - may have been called 2+ times
169 if name_mag not in self.fluxes:
170 name_found = None
171 drop_err = False
172 # Check if the name with errors is a configured default
173 if name_mag.endswith("_err"):
174 if hasattr(self.fluxes_default, name_mag):
175 name_found = name_mag
176 else:
177 if hasattr(self.fluxes_default, name_mag):
178 name_found = name_mag
179 # Check if a config with errors exists but not without
180 elif hasattr(self.fluxes_default, f"{name_mag}_err"):
181 name_found = f"{name_mag}_err"
182 # Don't load the errors - no _err suffix == unneeded
183 drop_err = True
184 if name_found:
185 # Copy the config - we don't want to edit in place
186 # Other instances may use them
187 value = copy.copy(getattr(self.fluxes_default, name_found))
188 # Ensure no unneeded error columns are loaded
189 if drop_err:
190 value.key_flux_error = None
191 self.fluxes[name_found] = value
192 self._add_flux(name=name_found, config=value)
193 else:
194 raise RuntimeError(
195 f"flux={name_mag} not defined in self.fluxes={self.fluxes}"
196 f" and no default configuration found"
197 )
198 if name_mag != name_found:
199 # Essentially appends _err to the name if needed
200 setattr(self, attr, name_found)
202 def finalize(self):
203 super().finalize()
204 for key, config in self.fluxes.items():
205 self._add_flux(name=key, config=config)
208class MagnitudeXTool(MagnitudeTool):
209 """A Tool metrics/plots with a magnitude as the dependent variable."""
211 mag_x = Field[str](default="", doc="Flux (magnitude) field to bin metrics or plot on x-axis")
213 @property
214 def config_mag_x(self):
215 if self.mag_x not in self.fluxes:
216 raise KeyError(f"{self.mag_x=} not in {self.fluxes}; was finalize called?")
217 # This is a logic error: it shouldn't be called before finalize
218 assert self.mag_x in self.fluxes
219 return self.fluxes[self.mag_x]
221 def finalize(self):
222 super().finalize()
223 self._set_flux_default("mag_x")
224 key_mag = f"mag_{self.mag_x}"
225 subsets = (("xAll", "allSelector"), ("xGalaxies", "galaxySelector"), ("xStars", "starSelector"))
226 for name, key in subsets:
227 self._set_action(
228 self.process.filterActions,
229 name,
230 DownselectVector,
231 vectorKey=key_mag,
232 selector=VectorSelector(vectorKey=key),
233 )
236class SizeConfig(Config):
237 """Configuration for size vector(s) to be loaded and possibly plotted."""
239 has_moments = Field[bool](doc="Whether this size measure is stored as 2D moments.", default=True)
240 key_size = Field[str](
241 doc="Size column(s) to compute/plot, including moment suffix as {suffix}.",
242 )
243 log10_size = Field[bool](
244 default=True,
245 doc="Whether to compute/plot)log10 of the sizes.",
246 )
247 name_size = Field[str](
248 default="size",
249 doc="Name of the size (e.g. for axis labels).",
250 )
251 scale_size = Field[float](
252 default=0.2,
253 doc="Factor to scale sizes (multiply) by.",
254 )
255 unit_size = Field[str](
256 default="arcsec",
257 doc="Unit for sizes.",
258 )
260 def modify_action(self, action: VectorAction) -> VectorAction:
261 if self.log10_size:
262 action = Log10Vector(actionA=action)
263 return action
266class SizeDefaultConfig(Config):
267 bulge = ConfigField[SizeConfig](doc="Bulge model size config.")
268 disk = ConfigField[SizeConfig](doc="Disk model size config.")
269 moments = ConfigField[SizeConfig](doc="Second moments size config.")
270 shape_slot = ConfigField[SizeConfig](doc="Shape slot size config.")
273class SizeTool(AnalysisTool):
274 """Compute various object size definitions in linear or log space."""
276 attr_prefix = Field[str](doc="Prefix to prepend to size names as attrs", default="size_", optional=False)
277 sizes_default = SizeDefaultConfig(
278 bulge=SizeConfig(key_size="{band}_bdReB", name_size="CModel Bulge $R_{eff}$", has_moments=False),
279 disk=SizeConfig(key_size="{band}_bdReD", name_size="CModel Disk $R_{eff}$", has_moments=False),
280 moments=SizeConfig(key_size="{band}_i{suffix}", name_size="Second moment radius"),
281 shape_slot=SizeConfig(key_size="shape_{suffix}", name_size="Shape slot radius"),
282 )
283 size_type = ChoiceField[str](
284 doc="The type of size to calculate",
285 allowed={
286 "determinantRadius": "The (matrix) determinant radius from x/y moments.",
287 "traceRadius": "The (matrix) trace radius from x/y moments.",
288 "singleColumnSize": "A pre-computed size from a single column.",
289 },
290 optional=False,
291 )
292 size_y = Field[str](default=None, doc="Name of size field to plot on y axis.")
293 sizes = ConfigDictField[str, SizeConfig]( # type: ignore
294 default={},
295 doc="Size fields to add to build actions",
296 )
298 def _check_attr(self, name_size: str):
299 """Check if a buildAction has already been set."""
300 attr = self.get_attr_name(name_size)
301 if hasattr(self.process.buildActions, attr):
302 raise RuntimeError(f"Can't re-set size build action with already-used {attr=} from {name_size=}")
304 def _get_action_determinant(self, config):
305 action = CalcMomentSize(
306 colXx=config.key_size.format(suffix="xx"),
307 colYy=config.key_size.format(suffix="yy"),
308 colXy=config.key_size.format(suffix="xy"),
309 )
310 return action
312 def _get_action_trace(self, config):
313 action = CalcMomentSize(
314 colXx=config.key_size.format(suffix="xx"), colYy=config.key_size.format(suffix="yy")
315 )
316 return action
318 def _get_action_single_column(self, config):
319 action = LoadVector(vectorKey=config.key_size)
320 return action
322 def get_attr_name(self, name_size):
323 """Return the build action attribute for a size of a given name."""
324 return f"{self.attr_prefix}{name_size}"
326 def setDefaults(self):
327 super().setDefaults()
328 self.produce.plot.legendLocation = "lower left"
330 def finalize(self):
331 super().finalize()
332 # A lazy check for whether finalize has already been called
333 if hasattr(self.process.filterActions, "yAll"):
334 return
335 if not self.size_y:
336 raise ValueError("Must specify size_y")
337 elif self.size_y not in self.sizes:
338 if size_y := getattr(self.sizes_default, self.size_y, None):
339 self.sizes[self.size_y] = size_y
340 else:
341 raise RuntimeError(f"{self.size_y=} not found in {self.sizes=} or {self.sizes_default=}")
343 if self.size_type == "determinantRadius":
344 get_action = self._get_action_determinant
345 elif self.size_type == "traceRadius":
346 get_action = self._get_action_trace
347 elif self.size_type == "singleColumnSize":
348 get_action = self._get_action_single_column
349 else:
350 raise ValueError(f"Unsupported {self.size_type=}")
352 for name, config in self.sizes.items():
353 self._check_attr(name)
354 action = config.modify_action(
355 MultiplyVector(
356 actionA=get_action(config=config),
357 actionB=ConstantValue(value=config.scale_size),
358 )
359 )
360 setattr(self.process.buildActions, self.get_attr_name(name), action)
362 attr = self.get_attr_name(self.size_y)
363 self.process.filterActions.yAll = DownselectVector(
364 vectorKey=attr, selector=VectorSelector(vectorKey="allSelector")
365 )
366 self.process.filterActions.yGalaxies = DownselectVector(
367 vectorKey=attr, selector=VectorSelector(vectorKey="galaxySelector")
368 )
369 self.process.filterActions.yStars = DownselectVector(
370 vectorKey=attr, selector=VectorSelector(vectorKey="starSelector")
371 )