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

176 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-16 04: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 

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

93 

94 def key_flux_band(self, band: str): 

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

96 

97 def key_flux_error_band(self, band: str): 

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

99 

100 

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

107 

108 

109class MagnitudeTool(AnalysisTool): 

110 """Compute magnitudes from flux columns. 

111 

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

116 

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 ) 

149 

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

151 default={}, 

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

153 ) 

154 

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

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

157 

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. 

167 

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 

188 

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

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

191 

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

215 

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. 

218 

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) 

268 

269 def finalize(self): 

270 super().finalize() 

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

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

273 

274 

275class MagnitudeXTool(MagnitudeTool): 

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

277 

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

279 

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] 

287 

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 ) 

301 

302 

303class SizeConfig(Config): 

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

305 

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 ) 

326 

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

328 if self.log10_size: 

329 action = Log10Vector(actionA=action) 

330 return action 

331 

332 

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

338 

339 

340class MomentsConfig(Config): 

341 """Configuration for moment field suffixes.""" 

342 

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

346 

347 

348class SizeTool(AnalysisTool): 

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

350 

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 ) 

380 

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

386 

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 

395 

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 

403 

404 def _get_action_single_column(self, config): 

405 action = LoadVector(vectorKey=config.key_size) 

406 return action 

407 

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

411 

412 def setDefaults(self): 

413 super().setDefaults() 

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

415 

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

428 

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

437 

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) 

447 

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 )