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