Coverage for python/lsst/analysis/tools/atools/genericBuild.py: 31%

162 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-06 04:18 -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 

22 

23__all__ = ("ExtendednessTool", "FluxConfig", "MagnitudeTool", "MagnitudeXTool", "SizeConfig", "SizeTool") 

24 

25import copy 

26 

27from lsst.pex.config import ChoiceField, Config, ConfigDictField, ConfigField, Field 

28from lsst.pex.config.configurableActions import ConfigurableActionStructField 

29 

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 

47 

48 

49class ExtendednessTool(AnalysisTool): 

50 """Select (non-)extended sources in visit/coadd contexts.""" 

51 

52 extendedness = Field[str]( 

53 default="refExtendedness", 

54 doc="Extendedness field to select sub-samples with", 

55 ) 

56 

57 parameterizedBand = Field[bool]( 

58 default=True, 

59 doc="Does this AnalysisTool support band as a name parameter", 

60 ) 

61 

62 def coaddContext(self) -> None: 

63 self.selectors.flagSelector = CoaddPlotFlagSelector() 

64 self.selectors.flagSelector.bands = ["{band}"] 

65 

66 def visitContext(self) -> None: 

67 self.parameterizedBand = False 

68 self.selectors.flagSelector = VisitPlotFlagSelector() 

69 

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) 

78 

79 

80class FluxConfig(Config): 

81 """Configuration for a flux vector to be loaded and potentially plotted.""" 

82 

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.") 

87 

88 def key_flux_band(self, band: str): 

89 return self.band_format.format(band=band, key=self.key_flux) 

90 

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)) 

93 

94 

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") 

101 

102 

103class MagnitudeTool(AnalysisTool): 

104 """Compute magnitudes from flux columns. 

105 

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 """ 

110 

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 ) 

120 

121 fluxes = ConfigDictField[str, FluxConfig]( # type: ignore 

122 default={}, 

123 doc="Flux fields to convert to magnitudes", 

124 ) 

125 

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) 

137 

138 def _set_action(self, target: ConfigurableActionStructField, name: str, action, *args, **kwargs): 

139 """Set an action attribute on a target tool's struct field. 

140 

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)) 

164 

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) 

201 

202 def finalize(self): 

203 super().finalize() 

204 for key, config in self.fluxes.items(): 

205 self._add_flux(name=key, config=config) 

206 

207 

208class MagnitudeXTool(MagnitudeTool): 

209 """A Tool metrics/plots with a magnitude as the dependent variable.""" 

210 

211 mag_x = Field[str](default="", doc="Flux (magnitude) field to bin metrics or plot on x-axis") 

212 

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] 

220 

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 ) 

234 

235 

236class SizeConfig(Config): 

237 """Configuration for size vector(s) to be loaded and possibly plotted.""" 

238 

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 ) 

259 

260 def modify_action(self, action: VectorAction) -> VectorAction: 

261 if self.log10_size: 

262 action = Log10Vector(actionA=action) 

263 return action 

264 

265 

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.") 

271 

272 

273class SizeTool(AnalysisTool): 

274 """Compute various object size definitions in linear or log space.""" 

275 

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 ) 

297 

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=}") 

303 

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 

311 

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 

317 

318 def _get_action_single_column(self, config): 

319 action = LoadVector(vectorKey=config.key_size) 

320 return action 

321 

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}" 

325 

326 def setDefaults(self): 

327 super().setDefaults() 

328 self.produce.legendLocation = "lower left" 

329 

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=}") 

342 

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=}") 

351 

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) 

361 

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 )