Coverage for python/lsst/meas/base/baseMeasurement.py: 26%

129 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-17 02:19 -0700

1# This file is part of meas_base. 

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/>. 

21 

22"""Base measurement task, which subclassed by the single frame and forced 

23measurement tasks. 

24""" 

25 

26import lsst.pipe.base 

27import lsst.pex.config 

28 

29from .pluginRegistry import PluginMap 

30from .exceptions import FatalAlgorithmError, MeasurementError 

31from .pluginsBase import BasePluginConfig, BasePlugin 

32from .noiseReplacer import NoiseReplacerConfig 

33 

34__all__ = ("BaseMeasurementPluginConfig", "BaseMeasurementPlugin", 

35 "BaseMeasurementConfig", "BaseMeasurementTask") 

36 

37# Exceptions that the measurement tasks should always propagate up to their 

38# callers 

39FATAL_EXCEPTIONS = (MemoryError, FatalAlgorithmError) 

40 

41 

42class BaseMeasurementPluginConfig(BasePluginConfig): 

43 """Base config class for all measurement plugins. 

44 

45 Notes 

46 ----- 

47 Most derived classes will want to override `setDefaults` in order to 

48 customize the default `executionOrder`. 

49 

50 A derived class whose corresponding Plugin class implements a do `measureN` 

51 method should additionally add a bool `doMeasureN` field to replace the 

52 bool class attribute defined here. 

53 """ 

54 

55 doMeasure = lsst.pex.config.Field(dtype=bool, default=True, 

56 doc="whether to run this plugin in single-object mode") 

57 

58 doMeasureN = False # replace this class attribute with a Field if measureN-capable 

59 

60 

61class BaseMeasurementPlugin(BasePlugin): 

62 """Base class for all measurement plugins. 

63 

64 Notes 

65 ----- 

66 This is class is a placeholder for future behavior which will be shared 

67 only between measurement plugins and is implemented for symmetry with the 

68 measurement base plugin configuration class 

69 """ 

70 

71 pass 

72 

73 

74class SourceSlotConfig(lsst.pex.config.Config): 

75 """Assign named plugins to measurement slots. 

76 

77 Slot configuration which assigns a particular named plugin to each of a set 

78 of slots. Each slot allows a type of measurement to be fetched from the 

79 `lsst.afw.table.SourceTable` without knowing which algorithm was used to 

80 produced the data. 

81 

82 Notes 

83 ----- 

84 The default algorithm for each slot must be registered, even if the default 

85 is not used. 

86 """ 

87 

88 Field = lsst.pex.config.Field 

89 centroid = Field(dtype=str, default="base_SdssCentroid", optional=True, 

90 doc="the name of the centroiding algorithm used to set source x,y") 

91 shape = Field(dtype=str, default="base_SdssShape", optional=True, 

92 doc="the name of the algorithm used to set source moments parameters") 

93 psfShape = Field(dtype=str, default="base_SdssShape_psf", optional=True, 

94 doc="the name of the algorithm used to set PSF moments parameters") 

95 apFlux = Field(dtype=str, default="base_CircularApertureFlux_12_0", optional=True, 

96 doc="the name of the algorithm used to set the source aperture instFlux slot") 

97 modelFlux = Field(dtype=str, default="base_GaussianFlux", optional=True, 

98 doc="the name of the algorithm used to set the source model instFlux slot") 

99 psfFlux = Field(dtype=str, default="base_PsfFlux", optional=True, 

100 doc="the name of the algorithm used to set the source psf instFlux slot") 

101 gaussianFlux = Field(dtype=str, default="base_GaussianFlux", optional=True, 

102 doc="the name of the algorithm used to set the source Gaussian instFlux slot") 

103 calibFlux = Field(dtype=str, default="base_CircularApertureFlux_12_0", optional=True, 

104 doc="the name of the instFlux measurement algorithm used for calibration") 

105 

106 def setupSchema(self, schema): 

107 """Set up a slots in a schema following configuration directives. 

108 

109 Parameters 

110 ---------- 

111 schema : `lsst.afw.table.Schema` 

112 The schema in which slots will be set up. 

113 

114 Notes 

115 ----- 

116 This is defined in this configuration class to support use in unit 

117 tests without needing to construct an `lsst.pipe.base.Task` object. 

118 """ 

119 aliases = schema.getAliasMap() 

120 if self.centroid is not None: 

121 aliases.set("slot_Centroid", self.centroid) 

122 if self.shape is not None: 

123 aliases.set("slot_Shape", self.shape) 

124 if self.psfShape is not None: 

125 aliases.set("slot_PsfShape", self.psfShape) 

126 if self.apFlux is not None: 

127 aliases.set("slot_ApFlux", self.apFlux) 

128 if self.modelFlux is not None: 

129 aliases.set("slot_ModelFlux", self.modelFlux) 

130 if self.psfFlux is not None: 

131 aliases.set("slot_PsfFlux", self.psfFlux) 

132 if self.gaussianFlux is not None: 

133 aliases.set("slot_GaussianFlux", self.gaussianFlux) 

134 if self.calibFlux is not None: 

135 aliases.set("slot_CalibFlux", self.calibFlux) 

136 

137 

138class BaseMeasurementConfig(lsst.pex.config.Config): 

139 """Base configuration for all measurement driver tasks. 

140 

141 Examples 

142 -------- 

143 Subclasses should define the 'plugins' and 'undeblended' registries, e.g. 

144 

145 .. code-block:: py 

146 

147 plugins = PluginBaseClass.registry.makeField( 

148 multi=True, 

149 default=[], 

150 doc="Plugins to be run and their configuration" 

151 ) 

152 undeblended = PluginBaseClass.registry.makeField( 

153 multi=True, 

154 default=[], 

155 doc="Plugins to run on undeblended image" 

156 ) 

157 

158 where ``PluginBaseClass`` is the appropriate base class of the plugin 

159 (e.g., `SingleFramePlugin` or `ForcedPlugin`). 

160 """ 

161 

162 slots = lsst.pex.config.ConfigField( 

163 dtype=SourceSlotConfig, 

164 doc="Mapping from algorithms to special aliases in Source." 

165 ) 

166 

167 doReplaceWithNoise = lsst.pex.config.Field( 

168 dtype=bool, default=True, optional=False, 

169 doc='When measuring, replace other detected footprints with noise?') 

170 

171 noiseReplacer = lsst.pex.config.ConfigField( 

172 dtype=NoiseReplacerConfig, 

173 doc="configuration that sets how to replace neighboring sources with noise" 

174 ) 

175 undeblendedPrefix = lsst.pex.config.Field( 

176 dtype=str, default="undeblended_", 

177 doc="Prefix to give undeblended plugins" 

178 ) 

179 

180 def validate(self): 

181 lsst.pex.config.Config.validate(self) 

182 if self.slots.centroid is not None and self.slots.centroid not in self.plugins.names: 

183 raise ValueError("source centroid slot algorithm is not being run.") 

184 if self.slots.shape is not None and self.slots.shape not in self.plugins.names: 

185 raise ValueError("source shape slot algorithm '%s' is not being run." % self.slots.shape) 

186 for slot in (self.slots.psfFlux, self.slots.apFlux, self.slots.modelFlux, 

187 self.slots.gaussianFlux, self.slots.calibFlux): 

188 if slot is not None: 

189 for name in self.plugins.names: 

190 if len(name) <= len(slot) and name == slot[:len(name)]: 

191 break 

192 else: 

193 raise ValueError("source instFlux slot algorithm '%s' is not being run." % slot) 

194 

195 

196class BaseMeasurementTask(lsst.pipe.base.Task): 

197 """Ultimate base class for all measurement tasks. 

198 

199 Parameters 

200 ---------- 

201 algMetadata : `lsst.daf.base.PropertyList` or `None` 

202 Will be modified in-place to contain metadata about the plugins being 

203 run. If `None`, an empty `~lsst.daf.base.PropertyList` will be 

204 created. 

205 **kwds 

206 Additional arguments passed to `lsst.pipe.base.Task.__init__`. 

207 

208 Notes 

209 ----- 

210 This base class for `SingleFrameMeasurementTask` and 

211 `ForcedMeasurementTask` mostly exists to share code between the two, and 

212 generally should not be used directly. 

213 """ 

214 

215 ConfigClass = BaseMeasurementConfig 

216 _DefaultName = "measurement" 

217 

218 plugins = None 

219 """Plugins to be invoked (`PluginMap`). 

220 

221 Initially empty, this will be populated as plugins are initialized. It 

222 should be considered read-only. 

223 """ 

224 

225 algMetadata = None 

226 """Metadata about active plugins (`lsst.daf.base.PropertyList`). 

227 

228 Contains additional information about active plugins to be saved with 

229 the output catalog. Will be filled by subclasses. 

230 """ 

231 

232 def __init__(self, algMetadata=None, **kwds): 

233 super(BaseMeasurementTask, self).__init__(**kwds) 

234 self.plugins = PluginMap() 

235 self.undeblendedPlugins = PluginMap() 

236 if algMetadata is None: 

237 algMetadata = lsst.daf.base.PropertyList() 

238 self.algMetadata = algMetadata 

239 

240 def initializePlugins(self, **kwds): 

241 """Initialize plugins (and slots) according to configuration. 

242 

243 Parameters 

244 ---------- 

245 **kwds 

246 Keyword arguments forwarded directly to plugin constructors. 

247 

248 Notes 

249 ----- 

250 Derived class constructors should call this method to fill the 

251 `plugins` attribute and add corresponding output fields and slot 

252 aliases to the output schema. 

253 

254 In addition to the attributes added by `BaseMeasurementTask.__init__`, 

255 a ``schema``` attribute holding the output schema must be present 

256 before this method is called. 

257 

258 Keyword arguments are forwarded directly to plugin constructors, 

259 allowing derived classes to use plugins with different signatures. 

260 """ 

261 # Make a place at the beginning for the centroid plugin to run first 

262 # (because it's an OrderedDict, adding an empty element in advance 

263 # means it will get run first when it's reassigned to the actual 

264 # Plugin). 

265 if self.config.slots.centroid is not None: 

266 self.plugins[self.config.slots.centroid] = None 

267 # Init the plugins, sorted by execution order. At the same time add to 

268 # the schema 

269 for executionOrder, name, config, PluginClass in sorted(self.config.plugins.apply()): 

270 # Pass logName to the plugin if the plugin is marked as using it 

271 # The task will use this name to log plugin errors, regardless. 

272 if getattr(PluginClass, "hasLogName", False): 

273 self.plugins[name] = PluginClass(config, name, metadata=self.algMetadata, 

274 logName=self.log.getChild(name).name, **kwds) 

275 else: 

276 self.plugins[name] = PluginClass(config, name, metadata=self.algMetadata, **kwds) 

277 

278 # In rare circumstances (usually tests), the centroid slot not be 

279 # coming from an algorithm, which means we'll have added something we 

280 # don't want to the plugins map, and we should remove it. 

281 if self.config.slots.centroid is not None and self.plugins[self.config.slots.centroid] is None: 

282 del self.plugins[self.config.slots.centroid] 

283 # Initialize the plugins to run on the undeblended image 

284 for executionOrder, name, config, PluginClass in sorted(self.config.undeblended.apply()): 

285 undeblendedName = self.config.undeblendedPrefix + name 

286 if getattr(PluginClass, "hasLogName", False): 

287 self.undeblendedPlugins[name] = PluginClass(config, undeblendedName, 

288 metadata=self.algMetadata, 

289 logName=self.log.getChild(undeblendedName).name, 

290 **kwds) 

291 else: 

292 self.undeblendedPlugins[name] = PluginClass(config, undeblendedName, 

293 metadata=self.algMetadata, **kwds) 

294 

295 def callMeasure(self, measRecord, *args, **kwds): 

296 """Call ``measure`` on all plugins and consistently handle exceptions. 

297 

298 Parameters 

299 ---------- 

300 measRecord : `lsst.afw.table.SourceRecord` 

301 The record corresponding to the object being measured. Will be 

302 updated in-place with the results of measurement. 

303 *args 

304 Positional arguments forwarded to ``plugin.measure`` 

305 **kwds 

306 Keyword arguments. Two are handled locally: 

307 

308 beginOrder : `int` 

309 Beginning execution order (inclusive). Measurements with 

310 ``executionOrder`` < ``beginOrder`` are not executed. `None` 

311 for no limit. 

312 

313 endOrder : `int` 

314 Ending execution order (exclusive). Measurements with 

315 ``executionOrder`` >= ``endOrder`` are not executed. `None` 

316 for no limit. 

317 

318 Others are forwarded to ``plugin.measure()``. 

319 

320 Notes 

321 ----- 

322 This method can be used with plugins that have different signatures; 

323 the only requirement is that ``measRecord`` be the first argument. 

324 Subsequent positional arguments and keyword arguments are forwarded 

325 directly to the plugin. 

326 

327 This method should be considered "protected": it is intended for use by 

328 derived classes, not users. 

329 """ 

330 beginOrder = kwds.pop("beginOrder", None) 

331 endOrder = kwds.pop("endOrder", None) 

332 for plugin in self.plugins.iter(): 

333 if beginOrder is not None and plugin.getExecutionOrder() < beginOrder: 

334 continue 

335 if endOrder is not None and plugin.getExecutionOrder() >= endOrder: 

336 break 

337 self.doMeasurement(plugin, measRecord, *args, **kwds) 

338 

339 def doMeasurement(self, plugin, measRecord, *args, **kwds): 

340 """Call ``measure`` on the specified plugin. 

341 

342 Exceptions are handled in a consistent way. 

343 

344 Parameters 

345 ---------- 

346 plugin : subclass of `BasePlugin` 

347 Plugin that will be executed. 

348 measRecord : `lsst.afw.table.SourceRecord` 

349 The record corresponding to the object being measured. Will be 

350 updated in-place with the results of measurement. 

351 *args 

352 Positional arguments forwarded to ``plugin.measure()``. 

353 **kwds 

354 Keyword arguments forwarded to ``plugin.measure()``. 

355 

356 Notes 

357 ----- 

358 This method can be used with plugins that have different signatures; 

359 the only requirement is that ``plugin`` and ``measRecord`` be the first 

360 two arguments. Subsequent positional arguments and keyword arguments 

361 are forwarded directly to the plugin. 

362 

363 This method should be considered "protected": it is intended for use by 

364 derived classes, not users. 

365 """ 

366 try: 

367 plugin.measure(measRecord, *args, **kwds) 

368 except FATAL_EXCEPTIONS: 

369 raise 

370 except MeasurementError as error: 

371 self.log.getChild(plugin.name).debug( 

372 "MeasurementError in %s.measure on record %s: %s", 

373 plugin.name, measRecord.getId(), error) 

374 plugin.fail(measRecord, error) 

375 except Exception as error: 

376 self.log.getChild(plugin.name).debug( 

377 "Exception in %s.measure on record %s: %s", 

378 plugin.name, measRecord.getId(), error) 

379 plugin.fail(measRecord) 

380 

381 def callMeasureN(self, measCat, *args, **kwds): 

382 """Call ``measureN`` on all plugins and consistently handle exceptions. 

383 

384 Parameters 

385 ---------- 

386 measCat : `lsst.afw.table.SourceCatalog` 

387 Catalog containing only the records for the source family to be 

388 measured, and where outputs should be written. 

389 *args 

390 Positional arguments forwarded to ``plugin.measure()`` 

391 **kwds 

392 Keyword arguments. Two are handled locally: 

393 

394 beginOrder: 

395 Beginning execution order (inclusive): Measurements with 

396 ``executionOrder`` < ``beginOrder`` are not executed. `None` 

397 for no limit. 

398 endOrder: 

399 Ending execution order (exclusive): measurements with 

400 ``executionOrder`` >= ``endOrder`` are not executed. `None` for 

401 no ``limit``. 

402 

403 Others are are forwarded to ``plugin.measure()``. 

404 

405 Notes 

406 ----- 

407 This method can be used with plugins that have different signatures; 

408 the only requirement is that ``measRecord`` be the first argument. 

409 Subsequent positional arguments and keyword arguments are forwarded 

410 directly to the plugin. 

411 

412 This method should be considered "protected": it is intended for use by 

413 derived classes, not users. 

414 """ 

415 beginOrder = kwds.pop("beginOrder", None) 

416 endOrder = kwds.pop("endOrder", None) 

417 for plugin in self.plugins.iterN(): 

418 if beginOrder is not None and plugin.getExecutionOrder() < beginOrder: 

419 continue 

420 if endOrder is not None and plugin.getExecutionOrder() >= endOrder: 

421 break 

422 self.doMeasurementN(plugin, measCat, *args, **kwds) 

423 

424 def doMeasurementN(self, plugin, measCat, *args, **kwds): 

425 """Call ``measureN`` on the specified plugin. 

426 

427 Exceptions are handled in a consistent way. 

428 

429 Parameters 

430 ---------- 

431 plugin : subclass of `BasePlugin` 

432 Plugin that will be executed. 

433 measCat : `lsst.afw.table.SourceCatalog` 

434 Catalog containing only the records for the source family to be 

435 measured, and where outputs should be written. 

436 *args 

437 Positional arguments forwarded to ``plugin.measureN()``. 

438 **kwds 

439 Keyword arguments forwarded to ``plugin.measureN()``. 

440 

441 Notes 

442 ----- 

443 This method can be used with plugins that have different signatures; 

444 the only requirement is that the ``plugin`` and ``measCat`` be the 

445 first two arguments. Subsequent positional arguments and keyword 

446 arguments are forwarded directly to the plugin. 

447 

448 This method should be considered "protected": it is intended for use by 

449 derived classes, not users. 

450 """ 

451 try: 

452 plugin.measureN(measCat, *args, **kwds) 

453 except FATAL_EXCEPTIONS: 

454 raise 

455 

456 except MeasurementError as error: 

457 for measRecord in measCat: 

458 self.log.getChild(plugin.name).debug( 

459 "MeasurementError in %s.measureN on records %s-%s: %s", 

460 plugin.name, measCat[0].getId(), measCat[-1].getId(), error) 

461 plugin.fail(measRecord, error) 

462 except Exception as error: 

463 for measRecord in measCat: 

464 plugin.fail(measRecord) 

465 self.log.getChild(plugin.name).debug( 

466 "Exception in %s.measureN on records %s-%s: %s", 

467 plugin.name, measCat[0].getId(), measCat[-1].getId(), error)