Coverage for python/lsst/meas/base/sfm.py: 29%

91 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 01:42 -0800

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 

22r"""Base classes for single-frame measurement plugins and the associated task. 

23 

24In single-frame measurement, we assume that detection and probably deblending 

25have already been run on the same frame, so a `~lsst.afw.table.SourceCatalog` 

26has already been created with `lsst.afw.detection.Footprint`\ s (which may be 

27"heavy" — that is, include pixel data). Measurements are generally recorded in 

28the coordinate system of the image being measured (and all slot-eligible 

29fields must be), but non-slot fields may be recorded in other coordinate 

30systems if necessary to avoid information loss (this should, of course, be 

31indicated in the field documentation). 

32""" 

33 

34from lsst.utils.logging import PeriodicLogger 

35from lsst.utils.timer import timeMethod 

36 

37from .pluginRegistry import PluginRegistry 

38from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin, 

39 BaseMeasurementConfig, BaseMeasurementTask) 

40from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer 

41 

42__all__ = ("SingleFramePluginConfig", "SingleFramePlugin", 

43 "SingleFrameMeasurementConfig", "SingleFrameMeasurementTask") 

44 

45 

46class SingleFramePluginConfig(BaseMeasurementPluginConfig): 

47 """Base class for single-frame plugin configuration classes. 

48 """ 

49 pass 

50 

51 

52class SingleFramePlugin(BaseMeasurementPlugin): 

53 """Base class for single-frame measurement plugin. 

54 

55 Parameters 

56 ---------- 

57 config : `SingleFramePlugin.ConfigClass` 

58 Configuration for this plugin. 

59 name : `str` 

60 The string with which the plugin was registered. 

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

62 The schema for the source table . New fields are added here to 

63 hold measurements produced by this plugin. 

64 metadata : `lsst.daf.base.PropertySet` 

65 Plugin metadata that will be attached to the output catalog 

66 logName : `str`, optional 

67 Name to use when logging errors. 

68 

69 Notes 

70 ----- 

71 New plugins can be created in Python by inheriting directly from this 

72 class and implementing the `measure`, `fail` (from `BasePlugin`), and 

73 optionally `__init__` and `measureN` methods. Plugins can also be defined 

74 in C++ via the `WrappedSingleFramePlugin` class. 

75 """ 

76 

77 registry = PluginRegistry(SingleFramePluginConfig) 

78 """Registry of subclasses of `SingleFramePlugin` (`PluginRegistry`). 

79 """ 

80 

81 ConfigClass = SingleFramePluginConfig 

82 

83 def __init__(self, config, name, schema, metadata, logName=None, **kwds): 

84 BaseMeasurementPlugin.__init__(self, config, name, logName=logName) 

85 

86 def measure(self, measRecord, exposure): 

87 """Measure the properties of a source on a single image. 

88 

89 The image may be from a single epoch, or it may be a coadd. 

90 

91 Parameters 

92 ---------- 

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

94 Record describing the object being measured. Previously-measured 

95 quantities may be retrieved from here, and it will be updated 

96 in-place tih the outputs of this plugin. 

97 exposure : `lsst.afw.image.ExposureF` 

98 The pixel data to be measured, together with the associated PSF, 

99 WCS, etc. All other sources in the image should have been replaced 

100 by noise according to deblender outputs. 

101 """ 

102 raise NotImplementedError() 

103 

104 def measureN(self, measCat, exposure): 

105 """Measure the properties of blended sources on a single image. 

106 

107 This operates on all members of a blend family at once. The image may 

108 be from a single epoch, or it may be a coadd. 

109 

110 Parameters 

111 ---------- 

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

113 Catalog describing the objects (and only those objects) being 

114 measured. Previously-measured quantities will be retrieved from 

115 here, and it will be updated in-place with the outputs of this 

116 plugin. 

117 exposure : `lsst.afw.image.ExposureF` 

118 The pixel data to be measured, together with the associated PSF, 

119 WCS, etc. All other sources in the image should have been replaced 

120 by noise according to deblender outputs. 

121 

122 Notes 

123 ----- 

124 Derived classes that do not implement ``measureN`` should just inherit 

125 this disabled version. Derived classes that do implement ``measureN`` 

126 should additionally add a bool doMeasureN config field to their config 

127 class to signal that measureN-mode is available. 

128 """ 

129 raise NotImplementedError() 

130 

131 

132class SingleFrameMeasurementConfig(BaseMeasurementConfig): 

133 """Config class for single frame measurement driver task. 

134 """ 

135 

136 plugins = SingleFramePlugin.registry.makeField( 

137 multi=True, 

138 default=["base_PixelFlags", 

139 "base_SdssCentroid", 

140 "base_SdssShape", 

141 "base_GaussianFlux", 

142 "base_PsfFlux", 

143 "base_CircularApertureFlux", 

144 "base_SkyCoord", 

145 "base_Variance", 

146 "base_Blendedness", 

147 "base_LocalBackground", 

148 "base_CompensatedTophatFlux", 

149 "base_ClassificationSizeExtendedness", 

150 ], 

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

152 ) 

153 algorithms = property(lambda self: self.plugins, doc="backwards-compatibility alias for plugins") 

154 undeblended = SingleFramePlugin.registry.makeField( 

155 multi=True, 

156 default=[], 

157 doc="Plugins to run on undeblended image" 

158 ) 

159 

160 

161class SingleFrameMeasurementTask(BaseMeasurementTask): 

162 """A subtask for measuring the properties of sources on a single exposure. 

163 

164 Parameters 

165 ---------- 

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

167 Schema of the output resultant catalog. Will be updated to provide 

168 fields to accept the outputs of plugins which will be executed by this 

169 task. 

170 algMetadata : `lsst.daf.base.PropertyList`, optional 

171 Used to record metadaa about algorithm execution. An empty 

172 `lsst.daf.base.PropertyList` will be created if `None`. 

173 **kwds 

174 Keyword arguments forwarded to `BaseMeasurementTask`. 

175 """ 

176 

177 ConfigClass = SingleFrameMeasurementConfig 

178 

179 NOISE_SEED_MULTIPLIER = "NOISE_SEED_MULTIPLIER" 

180 """Name by which the noise seed multiplier is recorded in metadata ('str'). 

181 """ 

182 

183 NOISE_SOURCE = "NOISE_SOURCE" 

184 """Name by which the noise source is recorded in metadata ('str'). 

185 """ 

186 

187 NOISE_OFFSET = "NOISE_OFFSET" 

188 """Name by which the noise offset is recorded in metadata ('str'). 

189 """ 

190 

191 NOISE_EXPOSURE_ID = "NOISE_EXPOSURE_ID" 

192 """Name by which the noise exposire ID is recorded in metadata ('str'). 

193 """ 

194 

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

196 super(SingleFrameMeasurementTask, self).__init__(algMetadata=algMetadata, **kwds) 

197 self.schema = schema 

198 self.config.slots.setupSchema(self.schema) 

199 self.initializePlugins(schema=self.schema) 

200 self.addInvalidPsfFlag(self.schema) 

201 

202 # Check to see if blendedness is one of the plugins 

203 if 'base_Blendedness' in self.plugins: 

204 self.doBlendedness = True 

205 self.blendPlugin = self.plugins['base_Blendedness'] 

206 else: 

207 self.doBlendedness = False 

208 

209 @timeMethod 

210 def run( 

211 self, 

212 measCat, 

213 exposure, 

214 noiseImage=None, 

215 exposureId=None, 

216 beginOrder=None, 

217 endOrder=None, 

218 footprints=None, 

219 ): 

220 r"""Run single frame measurement over an exposure and source catalog. 

221 

222 Parameters 

223 ---------- 

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

225 Catalog to be filled with the results of measurement. Must contain 

226 all the `lsst.afw.table.SourceRecord`\ s to be measured (with 

227 `lsst.afw.detection.Footprint`\ s attached), and have a schema 

228 that is a superset of ``self.schema``. 

229 exposure : `lsst.afw.image.ExposureF` 

230 Image containing the pixel data to be measured together with 

231 associated PSF, WCS, etc. 

232 noiseImage : `lsst.afw.image.ImageF`, optional 

233 Can be used to specify the a predictable noise replacement field 

234 for testing purposes. 

235 exposureId : `int`, optional 

236 Unique exposure identifier used to calculate the random number 

237 generator seed during noise replacement. 

238 beginOrder : `float`, optional 

239 Start execution order (inclusive): measurements with 

240 ``executionOrder < beginOrder`` are not executed. `None` for no 

241 limit. 

242 endOrder : `float`, optional 

243 Final execution order (exclusive): measurements with 

244 ``executionOrder >= endOrder`` are not executed. `None` for no 

245 limit. 

246 footprints : `dict` {`int`: `lsst.afw.detection.Footprint`}, optional 

247 List of footprints to use for noise replacement. If this is not 

248 supplied then the footprints from the measCat are used. 

249 """ 

250 assert measCat.getSchema().contains(self.schema) 

251 if footprints is None: 

252 footprints = self.getFootprintsFromCatalog(measCat) 

253 

254 # noiseReplacer is used to fill the footprints with noise and save 

255 # heavy footprints of the source pixels so that they can be restored 

256 # one at a time for measurement. After the NoiseReplacer is 

257 # constructed, all pixels in the exposure.getMaskedImage() which 

258 # belong to objects in measCat will be replaced with noise 

259 

260 if self.config.doReplaceWithNoise: 

261 noiseReplacer = NoiseReplacer(self.config.noiseReplacer, exposure, footprints, 

262 noiseImage=noiseImage, log=self.log, exposureId=exposureId) 

263 algMetadata = measCat.getMetadata() 

264 if algMetadata is not None: 

265 algMetadata.addInt(self.NOISE_SEED_MULTIPLIER, self.config.noiseReplacer.noiseSeedMultiplier) 

266 algMetadata.addString(self.NOISE_SOURCE, self.config.noiseReplacer.noiseSource) 

267 algMetadata.addDouble(self.NOISE_OFFSET, self.config.noiseReplacer.noiseOffset) 

268 if exposureId is not None: 

269 algMetadata.addLong(self.NOISE_EXPOSURE_ID, exposureId) 

270 else: 

271 noiseReplacer = DummyNoiseReplacer() 

272 

273 self.runPlugins(noiseReplacer, measCat, exposure, beginOrder, endOrder) 

274 

275 def runPlugins(self, noiseReplacer, measCat, exposure, beginOrder=None, endOrder=None): 

276 r"""Call the configured measument plugins on an image. 

277 

278 Parameters 

279 ---------- 

280 noiseReplacer : `NoiseReplacer` 

281 Used to fill sources not being measured with noise. 

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

283 Catalog to be filled with the results of measurement. Must contain 

284 all the `lsst.afw.table.SourceRecord`\ s to be measured (with 

285 `lsst.afw.detection.Footprint`\ s attached), and have a schema 

286 that is a superset of ``self.schema``. 

287 exposure : `lsst.afw.image.ExposureF` 

288 Image containing the pixel data to be measured together with 

289 associated PSF, WCS, etc. 

290 beginOrder : `float`, optional 

291 Start execution order (inclusive): measurements with 

292 ``executionOrder < beginOrder`` are not executed. `None` for no 

293 limit. 

294 endOrder : `float`, optional 

295 Final execution order (exclusive): measurements with 

296 ``executionOrder >= endOrder`` are not executed. `None` for no 

297 limit. 

298 """ 

299 # First, create a catalog of all parentless sources. Loop through all 

300 # the parent sources, first processing the children, then the parent. 

301 measParentCat = measCat.getChildren(0) 

302 

303 nMeasCat = len(measCat) 

304 nMeasParentCat = len(measParentCat) 

305 self.log.info("Measuring %d source%s (%d parent%s, %d child%s) ", 

306 nMeasCat, ("" if nMeasCat == 1 else "s"), 

307 nMeasParentCat, ("" if nMeasParentCat == 1 else "s"), 

308 nMeasCat - nMeasParentCat, ("" if nMeasCat - nMeasParentCat == 1 else "ren")) 

309 

310 # Wrap the task logger into a period logger 

311 periodicLog = PeriodicLogger(self.log) 

312 

313 childrenIter = measCat.getChildren([measParentRecord.getId() for measParentRecord in measParentCat]) 

314 for parentIdx, (measParentRecord, measChildCat) in enumerate(zip(measParentCat, childrenIter)): 

315 # first get all the children of this parent, insert footprint in 

316 # turn, and measure 

317 # TODO: skip this loop if there are no plugins configured for 

318 # single-object mode 

319 for measChildRecord in measChildCat: 

320 noiseReplacer.insertSource(measChildRecord.getId()) 

321 self.callMeasure(measChildRecord, exposure, beginOrder=beginOrder, endOrder=endOrder) 

322 

323 if self.doBlendedness: 

324 self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measChildRecord) 

325 

326 noiseReplacer.removeSource(measChildRecord.getId()) 

327 

328 # Then insert the parent footprint, and measure that 

329 noiseReplacer.insertSource(measParentRecord.getId()) 

330 self.callMeasure(measParentRecord, exposure, beginOrder=beginOrder, endOrder=endOrder) 

331 

332 if self.doBlendedness: 

333 self.blendPlugin.cpp.measureChildPixels(exposure.getMaskedImage(), measParentRecord) 

334 

335 # Finally, process both parent and child set through measureN 

336 self.callMeasureN(measParentCat[parentIdx:parentIdx+1], exposure, 

337 beginOrder=beginOrder, endOrder=endOrder) 

338 self.callMeasureN(measChildCat, exposure, beginOrder=beginOrder, endOrder=endOrder) 

339 noiseReplacer.removeSource(measParentRecord.getId()) 

340 # Log a message if it has been a while since the last log. 

341 periodicLog.log("Measurement complete for %d parents (and their children) out of %d", 

342 parentIdx + 1, nMeasParentCat) 

343 

344 # When done, restore the exposure to its original state 

345 noiseReplacer.end() 

346 

347 # Undeblended plugins only fire if we're running everything 

348 if endOrder is None: 

349 for sourceIndex, source in enumerate(measCat): 

350 for plugin in self.undeblendedPlugins.iter(): 

351 self.doMeasurement(plugin, source, exposure) 

352 # Log a message if it has been a while since the last log. 

353 periodicLog.log("Undeblended measurement complete for %d sources out of %d", 

354 sourceIndex + 1, nMeasCat) 

355 

356 # Now we loop over all of the sources one more time to compute the 

357 # blendedness metrics 

358 if self.doBlendedness: 

359 for source in measCat: 

360 self.blendPlugin.cpp.measureParentPixels(exposure.getMaskedImage(), source) 

361 

362 def measure(self, measCat, exposure): 

363 """Backwards-compatibility alias for `run`. 

364 """ 

365 self.run(measCat, exposure)