Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

34import lsst.pex.config 

35import time 

36from lsst.utils.timer import timeMethod 

37 

38from .pluginRegistry import PluginRegistry 

39from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin, 

40 BaseMeasurementConfig, BaseMeasurementTask) 

41from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer 

42 

43__all__ = ("SingleFramePluginConfig", "SingleFramePlugin", 

44 "SingleFrameMeasurementConfig", "SingleFrameMeasurementTask") 

45 

46 

47class SingleFramePluginConfig(BaseMeasurementPluginConfig): 

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

49 """ 

50 pass 

51 

52 

53class SingleFramePlugin(BaseMeasurementPlugin): 

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

55 

56 Parameters 

57 ---------- 

58 config : `SingleFramePlugin.ConfigClass` 

59 Configuration for this plugin. 

60 name : `str` 

61 The string with which the plugin was registered. 

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

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

64 hold measurements produced by this plugin. 

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

66 Plugin metadata that will be attached to the output catalog 

67 logName : `str`, optional 

68 Name to use when logging errors. 

69 

70 Notes 

71 ----- 

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

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

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

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

76 """ 

77 

78 registry = PluginRegistry(SingleFramePluginConfig) 

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

80 """ 

81 

82 ConfigClass = SingleFramePluginConfig 

83 

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

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

86 

87 def measure(self, measRecord, exposure): 

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

89 

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

91 

92 Parameters 

93 ---------- 

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

95 Record describing the object being measured. Previously-measured 

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

97 in-place tih the outputs of this plugin. 

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

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

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

101 by noise according to deblender outputs. 

102 """ 

103 raise NotImplementedError() 

104 

105 def measureN(self, measCat, exposure): 

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

107 

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

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

110 

111 Parameters 

112 ---------- 

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

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

115 measured. Previously-measured quantities will be retrieved from 

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

117 plugin. 

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

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

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

121 by noise according to deblender outputs. 

122 

123 Notes 

124 ----- 

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

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

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

128 class to signal that measureN-mode is available. 

129 """ 

130 raise NotImplementedError() 

131 

132 

133class SingleFrameMeasurementConfig(BaseMeasurementConfig): 

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

135 """ 

136 

137 plugins = SingleFramePlugin.registry.makeField( 

138 multi=True, 

139 default=["base_PixelFlags", 

140 "base_SdssCentroid", 

141 "base_NaiveCentroid", 

142 "base_SdssShape", 

143 "base_GaussianFlux", 

144 "base_PsfFlux", 

145 "base_CircularApertureFlux", 

146 "base_SkyCoord", 

147 "base_Variance", 

148 "base_Blendedness", 

149 "base_LocalBackground", 

150 ], 

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

152 ) 

153 algorithms = property(lambda self: self.plugins, doc="backwards-compatibility alias for plugins") 153 ↛ exitline 153 didn't run the lambda on line 153

154 undeblended = SingleFramePlugin.registry.makeField( 

155 multi=True, 

156 default=[], 

157 doc="Plugins to run on undeblended image" 

158 ) 

159 loggingInterval = lsst.pex.config.Field( 

160 dtype=int, 

161 default=600, 

162 doc="Interval (in seconds) to log messages (at VERBOSE level) while running measurement plugins." 

163 ) 

164 

165 

166class SingleFrameMeasurementTask(BaseMeasurementTask): 

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

168 

169 Parameters 

170 ---------- 

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

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

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

174 task. 

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

176 Used to record metadaa about algorithm execution. An empty 

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

178 **kwds 

179 Keyword arguments forwarded to `BaseMeasurementTask`. 

180 """ 

181 

182 ConfigClass = SingleFrameMeasurementConfig 

183 

184 NOISE_SEED_MULTIPLIER = "NOISE_SEED_MULTIPLIER" 

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

186 """ 

187 

188 NOISE_SOURCE = "NOISE_SOURCE" 

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

190 """ 

191 

192 NOISE_OFFSET = "NOISE_OFFSET" 

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

194 """ 

195 

196 NOISE_EXPOSURE_ID = "NOISE_EXPOSURE_ID" 

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

198 """ 

199 

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

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

202 self.schema = schema 

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

204 self.initializePlugins(schema=self.schema) 

205 

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

207 if 'base_Blendedness' in self.plugins: 

208 self.doBlendedness = True 

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

210 else: 

211 self.doBlendedness = False 

212 

213 @timeMethod 

214 def run(self, measCat, exposure, noiseImage=None, exposureId=None, beginOrder=None, endOrder=None): 

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

216 

217 Parameters 

218 ---------- 

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

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

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

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

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

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

225 Image containing the pixel data to be measured together with 

226 associated PSF, WCS, etc. 

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

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

229 for testing purposes. 

230 exposureId : `int`, optional 

231 Unique exposure identifier used to calculate the random number 

232 generator seed during noise replacement. 

233 beginOrder : `float`, optional 

234 Start execution order (inclusive): measurements with 

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

236 limit. 

237 endOrder : `float`, optional 

238 Final execution order (exclusive): measurements with 

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

240 limit. 

241 """ 

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

243 footprints = {measRecord.getId(): (measRecord.getParent(), measRecord.getFootprint()) 

244 for measRecord in measCat} 

245 

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

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

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

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

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

251 

252 if self.config.doReplaceWithNoise: 

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

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

255 algMetadata = measCat.getMetadata() 

256 if algMetadata is not None: 

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

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

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

260 if exposureId is not None: 

261 algMetadata.addLong(self.NOISE_EXPOSURE_ID, exposureId) 

262 else: 

263 noiseReplacer = DummyNoiseReplacer() 

264 

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

266 

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

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

269 

270 Parameters 

271 ---------- 

272 noiseReplacer : `NoiseReplacer` 

273 Used to fill sources not being measured with noise. 

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

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

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

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

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

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

280 Image containing the pixel data to be measured together with 

281 associated PSF, WCS, etc. 

282 beginOrder : `float`, optional 

283 Start execution order (inclusive): measurements with 

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

285 limit. 

286 endOrder : `float`, optional 

287 Final execution order (exclusive): measurements with 

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

289 limit. 

290 """ 

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

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

293 measParentCat = measCat.getChildren(0) 

294 

295 nMeasCat = len(measCat) 

296 nMeasParentCat = len(measParentCat) 

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

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

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

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

301 nextLogTime = time.time() + self.config.loggingInterval 

302 

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

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

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

306 # turn, and measure 

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

308 # single-object mode 

309 for measChildRecord in measChildCat: 

310 noiseReplacer.insertSource(measChildRecord.getId()) 

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

312 

313 if self.doBlendedness: 

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

315 

316 noiseReplacer.removeSource(measChildRecord.getId()) 

317 

318 # Then insert the parent footprint, and measure that 

319 noiseReplacer.insertSource(measParentRecord.getId()) 

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

321 

322 if self.doBlendedness: 

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

324 

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

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

327 beginOrder=beginOrder, endOrder=endOrder) 

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

329 noiseReplacer.removeSource(measParentRecord.getId()) 

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

331 if (currentTime := time.time()) > nextLogTime: 

332 self.log.verbose("Measurement complete for %d parents (and their children) out of %d", 

333 parentIdx + 1, nMeasParentCat) 

334 nextLogTime = currentTime + self.config.loggingInterval 

335 

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

337 noiseReplacer.end() 

338 

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

340 if endOrder is None: 

341 for sourceIndex, source in enumerate(measCat): 

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

343 self.doMeasurement(plugin, source, exposure) 

344 if (currentTime := time.time()) > nextLogTime: 

345 self.log.verbose("Undeblended measurement complete for %d sources out of %d", 

346 sourceIndex + 1, nMeasCat) 

347 nextLogTime = currentTime + self.config.loggingInterval 

348 

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

350 # blendedness metrics 

351 if self.doBlendedness: 

352 for source in measCat: 

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

354 

355 def measure(self, measCat, exposure): 

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

357 """ 

358 self.run(measCat, exposure)