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

175 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-24 04:10 -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 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.") 

86 

87 def key_flux_band(self, band: str): 

88 return self.key_flux.format(band=band) 

89 

90 def key_flux_error_band(self, band: str): 

91 return self.key_flux_error.format(band=band) 

92 

93 

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

100 

101 

102class MagnitudeTool(AnalysisTool): 

103 """Compute magnitudes from flux columns. 

104 

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

109 

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 ) 

127 

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

129 default={}, 

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

131 ) 

132 

133 def _add_flux(self, name: str, config: FluxConfig, band: str | None = None) -> str: 

134 """Add requisite buildActions for a given flux. 

135 

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. 

145 

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 

166 

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

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

169 

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

193 

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. 

196 

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) 

246 

247 def finalize(self): 

248 super().finalize() 

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

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

251 

252 

253class MagnitudeXTool(MagnitudeTool): 

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

255 

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

257 

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] 

265 

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 ) 

279 

280 

281class SizeConfig(Config): 

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

283 

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 ) 

304 

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

306 if self.log10_size: 

307 action = Log10Vector(actionA=action) 

308 return action 

309 

310 

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

316 

317 

318class MomentsConfig(Config): 

319 """Configuration for moment field suffixes.""" 

320 

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

324 

325 

326class SizeTool(AnalysisTool): 

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

328 

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 ) 

358 

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

364 

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 

373 

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 

381 

382 def _get_action_single_column(self, config): 

383 action = LoadVector(vectorKey=config.key_size) 

384 return action 

385 

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

389 

390 def setDefaults(self): 

391 super().setDefaults() 

392 self.produce.plot.legendLocation = "lower left" 

393 

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

406 

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

415 

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) 

425 

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 )