Coverage for python/lsst/analysis/tools/atools/genericBuild.py: 31%
175 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 04:19 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 04:19 -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 key_flux = Field[str](default=None, doc="Format of the flux field to convert to magnitudes with {band}.")
84 key_flux_error = Field[str](default=None, doc="Format of the flux error field.", optional=True)
85 name_flux = Field[str](default=None, doc="Name of the flux/magnitude algorithm/model.")
87 def key_flux_band(self, band: str):
88 return self.key_flux.format(band=band)
90 def key_flux_error_band(self, band: str):
91 return self.key_flux_error.format(band=band)
94class FluxesDefaultConfig(Config):
95 bulge_err = ConfigField[FluxConfig](doc="Bulge model magnitude with errors")
96 cmodel_err = ConfigField[FluxConfig](doc="CModel total magnitude with errors")
97 disk_err = ConfigField[FluxConfig](doc="Disk model magnitude with errors")
98 psf_err = ConfigField[FluxConfig](doc="PSF model magnitude with errors")
99 ref_matched = ConfigField[FluxConfig](doc="Reference catalog magnitude")
102class MagnitudeTool(AnalysisTool):
103 """Compute magnitudes from flux columns.
105 Any tool that reads in flux columns and converts them to magnitudes can
106 derive from this class and use the _add_flux method to set the
107 necessary build actions in their own finalize() methods.
108 """
110 fluxes_default = FluxesDefaultConfig(
111 bulge_err=FluxConfig(
112 key_flux="{band}_bdFluxB", key_flux_error="{band}_bdFluxBErr", name_flux="CModel Bulge"
113 ),
114 cmodel_err=FluxConfig(
115 key_flux="{band}_cModelFlux", key_flux_error="{band}_cModelFluxErr", name_flux="CModel"
116 ),
117 disk_err=FluxConfig(
118 key_flux="{band}_bdFluxD", key_flux_error="{band}_bdFluxDErr", name_flux="CModel Disk"
119 ),
120 psf_err=FluxConfig(key_flux="{band}_psfFlux", key_flux_error="{band}_psfFluxErr", name_flux="PSF"),
121 ref_matched=FluxConfig(
122 key_flux="refcat_flux_{band}",
123 name_flux="Reference",
124 key_flux_error=None,
125 ),
126 )
128 fluxes = ConfigDictField[str, FluxConfig]( # type: ignore
129 default={},
130 doc="Flux fields to convert to magnitudes",
131 )
133 def _add_flux(self, name: str, config: FluxConfig, band: str | None = None) -> str:
134 """Add requisite buildActions for a given flux.
136 Parameters
137 ----------
138 name
139 The name of the flux, without "flux_" prefix.
140 config
141 The configuration for the flux.
142 band
143 The name of the band. Default "{band}" assumes the this band is
144 the parameterized band.
146 Returns
147 -------
148 name
149 The name of the flux, suffixed by band if band is not None.
150 """
151 if band is None:
152 band = "{band}"
153 else:
154 name = f"{name}_{band}"
155 key_flux = config.key_flux_band(band=band)
156 name_flux = f"flux_{name}"
157 self._set_action(self.process.buildActions, name_flux, LoadVector, vectorKey=key_flux)
158 if config.key_flux_error is not None:
159 # Pre-emptively loaded for e.g. future S/N calculations
160 key_flux_err = config.key_flux_error_band(band=band)
161 self._set_action(
162 self.process.buildActions, f"flux_err_{name}", LoadVector, vectorKey=key_flux_err
163 )
164 self._set_action(self.process.buildActions, f"mag_{name}", ConvertFluxToMag, vectorKey=key_flux)
165 return name
167 def _set_action(self, target: ConfigurableActionStructField, name: str, action, *args, **kwargs):
168 """Set an action attribute on a target tool's struct field.
170 Parameters
171 ----------
172 target
173 The ConfigurableActionStructField to set an attribute on.
174 name
175 The name of the attribute to set.
176 action
177 The action class to set the attribute to.
178 args
179 Arguments to pass when initialization the action.
180 kwargs
181 Keyword arguments to pass when initialization the action.
182 """
183 if hasattr(target, name):
184 attr = getattr(target, name)
185 # Setting an attr to a different action is a logic error
186 assert isinstance(attr, action)
187 # Assert that the action's attributes are identical
188 for key, value in kwargs.items():
189 if value.__class__.__module__ == "__builtin__":
190 assert getattr(attr, key) == value
191 else:
192 setattr(target, name, action(*args, **kwargs))
194 def _set_flux_default(self: str, attr, band: str | None = None, name_mag: str | None = None):
195 """Set own config attr to appropriate string flux name.
197 Parameters
198 ----------
199 attr
200 The name of the attribute to set.
201 band
202 The name of the band to pass to _add_flux.
203 name_mag
204 The name of the magnitude to configure. If None, self must already
205 have an attr, and name_mag is set to the attr's value.
206 """
207 name_mag_is_none = name_mag is None
208 if name_mag_is_none:
209 name_mag = getattr(self, attr)
210 complete = name_mag in self.fluxes
211 else:
212 complete = hasattr(self, attr)
213 # Do nothing if already set - may have been called 2+ times
214 if not complete:
215 name_found = None
216 drop_err = False
217 # Check if the name with errors is a configured default
218 if name_mag.endswith("_err"):
219 if hasattr(self.fluxes_default, name_mag):
220 name_found = name_mag
221 else:
222 if hasattr(self.fluxes_default, name_mag):
223 name_found = name_mag
224 # Check if a config with errors exists but not without
225 elif hasattr(self.fluxes_default, f"{name_mag}_err"):
226 name_found = f"{name_mag}_err"
227 # Don't load the errors - no _err suffix == unneeded
228 drop_err = True
229 if name_found:
230 # Copy the config - we don't want to edit in place
231 # Other instances may use them
232 value = copy.copy(getattr(self.fluxes_default, name_found))
233 # Ensure no unneeded error columns are loaded
234 if drop_err:
235 value.key_flux_error = None
236 self.fluxes[name_found] = value
237 name_found = self._add_flux(name=name_found, config=value, band=band)
238 else:
239 raise RuntimeError(
240 f"flux={name_mag} not defined in self.fluxes={self.fluxes}"
241 f" and no default configuration found"
242 )
243 if name_mag_is_none and (name_mag != name_found):
244 # Essentially appends _err to the name if needed
245 setattr(self, attr, name_found)
247 def finalize(self):
248 super().finalize()
249 for key, config in self.fluxes.items():
250 self._add_flux(name=key, config=config)
253class MagnitudeXTool(MagnitudeTool):
254 """A Tool metrics/plots with a magnitude as the dependent variable."""
256 mag_x = Field[str](default="", doc="Flux (magnitude) field to bin metrics or plot on x-axis")
258 @property
259 def config_mag_x(self):
260 if self.mag_x not in self.fluxes:
261 raise KeyError(f"{self.mag_x=} not in {self.fluxes}; was finalize called?")
262 # This is a logic error: it shouldn't be called before finalize
263 assert self.mag_x in self.fluxes
264 return self.fluxes[self.mag_x]
266 def finalize(self):
267 super().finalize()
268 self._set_flux_default("mag_x")
269 key_mag = f"mag_{self.mag_x}"
270 subsets = (("xAll", "allSelector"), ("xGalaxies", "galaxySelector"), ("xStars", "starSelector"))
271 for name, key in subsets:
272 self._set_action(
273 self.process.filterActions,
274 name,
275 DownselectVector,
276 vectorKey=key_mag,
277 selector=VectorSelector(vectorKey=key),
278 )
281class SizeConfig(Config):
282 """Configuration for size vector(s) to be loaded and possibly plotted."""
284 has_moments = Field[bool](doc="Whether this size measure is stored as 2D moments.", default=True)
285 key_size = Field[str](
286 doc="Size column(s) to compute/plot, including moment suffix as {suffix}.",
287 )
288 log10_size = Field[bool](
289 default=True,
290 doc="Whether to compute/plot)log10 of the sizes.",
291 )
292 name_size = Field[str](
293 default="size",
294 doc="Name of the size (e.g. for axis labels).",
295 )
296 scale_size = Field[float](
297 default=0.2,
298 doc="Factor to scale sizes (multiply) by.",
299 )
300 unit_size = Field[str](
301 default="arcsec",
302 doc="Unit for sizes.",
303 )
305 def modify_action(self, action: VectorAction) -> VectorAction:
306 if self.log10_size:
307 action = Log10Vector(actionA=action)
308 return action
311class SizeDefaultConfig(Config):
312 bulge = ConfigField[SizeConfig](doc="Bulge model size config.")
313 disk = ConfigField[SizeConfig](doc="Disk model size config.")
314 moments = ConfigField[SizeConfig](doc="Second moments size config.")
315 shape_slot = ConfigField[SizeConfig](doc="Shape slot size config.")
318class MomentsConfig(Config):
319 """Configuration for moment field suffixes."""
321 xx = Field[str](doc="Suffix for the x/xx moments.", default="xx")
322 xy = Field[str](doc="Suffix for the rho value/xy moments.", default="xy")
323 yy = Field[str](doc="Suffix for the y/yy moments.", default="yy")
326class SizeTool(AnalysisTool):
327 """Compute various object size definitions in linear or log space."""
329 attr_prefix = Field[str](doc="Prefix to prepend to size names as attrs", default="size_", optional=False)
330 config_moments = ConfigField[MomentsConfig](
331 doc="Configuration for moment field names", default=MomentsConfig
332 )
333 is_covariance = Field[bool](
334 doc="Whether this size has multiple fields as for a covariance matrix."
335 " If False, the XX/YY/XY terms are instead assumed to map to sigma_x/sigma_y/rho.",
336 default=True,
337 )
338 sizes_default = SizeDefaultConfig(
339 bulge=SizeConfig(key_size="{band}_bdReB", name_size="CModel Bulge $R_{eff}$", has_moments=False),
340 disk=SizeConfig(key_size="{band}_bdReD", name_size="CModel Disk $R_{eff}$", has_moments=False),
341 moments=SizeConfig(key_size="{band}_i{suffix}", name_size="Second moment radius"),
342 shape_slot=SizeConfig(key_size="shape_{suffix}", name_size="Shape slot radius"),
343 )
344 size_type = ChoiceField[str](
345 doc="The type of size to calculate",
346 allowed={
347 "determinantRadius": "The (matrix) determinant radius from x/y moments.",
348 "traceRadius": "The (matrix) trace radius from x/y moments.",
349 "singleColumnSize": "A pre-computed size from a single column.",
350 },
351 optional=False,
352 )
353 size_y = Field[str](default=None, doc="Name of size field to plot on y axis.")
354 sizes = ConfigDictField[str, SizeConfig]( # type: ignore
355 default={},
356 doc="Size fields to add to build actions",
357 )
359 def _check_attr(self, name_size: str):
360 """Check if a buildAction has already been set."""
361 attr = self.get_attr_name(name_size)
362 if hasattr(self.process.buildActions, attr):
363 raise RuntimeError(f"Can't re-set size build action with already-used {attr=} from {name_size=}")
365 def _get_action_determinant(self, config):
366 action = CalcMomentSize(
367 colXx=config.key_size.format(suffix=self.config_moments.xx),
368 colYy=config.key_size.format(suffix=self.config_moments.yy),
369 colXy=config.key_size.format(suffix=self.config_moments.xy),
370 is_covariance=self.is_covariance,
371 )
372 return action
374 def _get_action_trace(self, config):
375 action = CalcMomentSize(
376 colXx=config.key_size.format(suffix=self.config_moments.xx),
377 colYy=config.key_size.format(suffix=self.config_moments.yy),
378 is_covariance=self.is_covariance,
379 )
380 return action
382 def _get_action_single_column(self, config):
383 action = LoadVector(vectorKey=config.key_size)
384 return action
386 def get_attr_name(self, name_size):
387 """Return the build action attribute for a size of a given name."""
388 return f"{self.attr_prefix}{name_size}"
390 def setDefaults(self):
391 super().setDefaults()
392 self.produce.plot.legendLocation = "lower left"
394 def finalize(self):
395 super().finalize()
396 # A lazy check for whether finalize has already been called
397 if hasattr(self.process.filterActions, "yAll"):
398 return
399 if not self.size_y:
400 raise ValueError("Must specify size_y")
401 elif self.size_y not in self.sizes:
402 if size_y := getattr(self.sizes_default, self.size_y, None):
403 self.sizes[self.size_y] = size_y
404 else:
405 raise RuntimeError(f"{self.size_y=} not found in {self.sizes=} or {self.sizes_default=}")
407 if self.size_type == "determinantRadius":
408 get_action = self._get_action_determinant
409 elif self.size_type == "traceRadius":
410 get_action = self._get_action_trace
411 elif self.size_type == "singleColumnSize":
412 get_action = self._get_action_single_column
413 else:
414 raise ValueError(f"Unsupported {self.size_type=}")
416 for name, config in self.sizes.items():
417 self._check_attr(name)
418 action = config.modify_action(
419 MultiplyVector(
420 actionA=get_action(config=config),
421 actionB=ConstantValue(value=config.scale_size),
422 )
423 )
424 setattr(self.process.buildActions, self.get_attr_name(name), action)
426 attr = self.get_attr_name(self.size_y)
427 self.process.filterActions.yAll = DownselectVector(
428 vectorKey=attr, selector=VectorSelector(vectorKey="allSelector")
429 )
430 self.process.filterActions.yGalaxies = DownselectVector(
431 vectorKey=attr, selector=VectorSelector(vectorKey="galaxySelector")
432 )
433 self.process.filterActions.yStars = DownselectVector(
434 vectorKey=attr, selector=VectorSelector(vectorKey="starSelector")
435 )