Coverage for python/lsst/meas/extensions/convolved/convolved.py: 31%

156 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-07 18:32 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008-2016 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import math 

24import numpy as np 

25 

26from lsst.pex.config import Config, Field, ListField, ConfigField, makeConfigClass 

27from lsst.pipe.base import Struct 

28from lsst.meas.extensions.photometryKron import KronAperture, KronFluxPlugin 

29from lsst.meas.base.wrappers import WrappedSingleFramePlugin, WrappedForcedPlugin 

30 

31import lsst.meas.base 

32import lsst.afw.math 

33import lsst.afw.image 

34import lsst.geom 

35from lsst.afw.geom.skyWcs import makeWcsPairTransform 

36 

37__all__ = ("SingleFrameConvolvedFluxPlugin", "SingleFrameConvolvedFluxConfig", 

38 "ForcedConvolvedFluxPlugin", "ForcedConvolvedFluxConfig",) 

39 

40 

41SIGMA_TO_FWHM = 2.0*math.sqrt(2.0*(math.log(2.0))) # Multiply sigma by this to get FWHM 

42PLUGIN_NAME = "ext_convolved_ConvolvedFlux" # Usual name for plugin 

43 

44 

45class DeconvolutionError(RuntimeError): 

46 """Convolving to the target seeing would require deconvolution""" 

47 pass 

48 

49 

50ApertureFluxConfig = makeConfigClass(lsst.meas.base.ApertureFluxControl) 

51 

52 

53class ConvolvedFluxData(Struct): 

54 """A `lsst.pipe.base.Struct` for convolved fluxes 

55 

56 Attributes 

57 ---------- 

58 deconvKey : `lsst.afw.table.Key_Flag` 

59 Key to set flag indicating no measurement was made due to the need to deconvolve 

60 aperture : `lsst.meas.base.CircularApertureFluxAlgorithm` 

61 Measurement algorithm to perform aperture flux measurements 

62 kronKeys : `lsst.pipe.base.Struct` 

63 Container for Kron results or `None` if no Kron radius is available; when set, 

64 includes `result` (`lsst.meas.base.FluxResultKey`: keys to set results from Kron 

65 flux measurement) and `flag` (`lsst.afw.table.Key_Flag`: key to set failure flag 

66 for Kron measurement). 

67 """ 

68 

69 def __init__(self, name, schema, seeing, config, metadata): 

70 deconvKey = schema.addField(name + "_deconv", type="Flag", 

71 doc="deconvolution required for seeing %f; no measurement made" % 

72 (seeing,)) 

73 aperture = lsst.meas.base.CircularApertureFluxAlgorithm(config.aperture.makeControl(), name, 

74 schema, metadata) 

75 kronKeys = Struct( 

76 result=lsst.meas.base.FluxResultKey.addFields(schema, name + "_kron", 

77 doc="convolved Kron flux: seeing %f" % (seeing,)), 

78 flag=schema.addField(name + "_kron_flag", type="Flag", 

79 doc="convolved Kron flux failed: seeing %f" % (seeing,)), 

80 ) 

81 Struct.__init__(self, deconvKey=deconvKey, aperture=aperture, kronKeys=kronKeys) 

82 

83 

84class BaseConvolvedFluxConfig(Config): 

85 # convolution 

86 seeing = ListField(dtype=float, default=[3.5, 5.0, 6.5], doc="list of target seeings (FWHM, pixels)") 

87 kernelScale = Field(dtype=float, default=4.0, doc="scaling factor of kernel sigma for kernel size") 

88 # aperture flux 

89 aperture = ConfigField(dtype=ApertureFluxConfig, doc="Aperture photometry parameters") 

90 # Kron flux 

91 kronRadiusName = Field(dtype=str, default="ext_photometryKron_KronFlux_radius", 

92 doc="name of Kron radius field in reference") 

93 maxSincRadius = Field(dtype=float, default=10.0, 

94 doc="Largest aperture for which to use the sinc aperture code for Kron (pixels)") 

95 kronRadiusForFlux = Field(dtype=float, default=2.5, doc="Number of Kron radii for Kron flux") 

96 registerForApCorr = Field(dtype=bool, default=True, 

97 doc="Register measurements for aperture correction?\n" 

98 "The aperture correction registration is done when the plugin is\n" 

99 "instantiated because the column names are derived from the configuration\n" 

100 "rather than being static. Sometimes you want to turn this off, e.g.,\n" 

101 "when you will use aperture corrections derived from somewhere else\n" 

102 "through the 'proxy' mechanism.") 

103 

104 def setDefaults(self): 

105 Config.setDefaults(self) 

106 # Don't need the full set of apertures because the larger ones aren't affected by the convolution 

107 self.aperture.radii = [3.3, 4.5, 6.0] 

108 

109 def getBaseNameForSeeing(self, seeing, name=PLUGIN_NAME): 

110 """Return base name for measurement, given seeing 

111 

112 Parameters 

113 ---------- 

114 seeing : `float` 

115 The seeing value; it is required that the `ConvolvedFluxConfig.seeing` list 

116 include this value. 

117 name : `str`, optional 

118 The name of the plugin. 

119 

120 Returns 

121 ------- 

122 baseName : `str` 

123 Base name for measurement with nominated seeing. 

124 """ 

125 indices = [ii for ii, target in enumerate(self.seeing) if seeing == target] 

126 if len(indices) != 1: 

127 raise RuntimeError("Unable to uniquely identify index for seeing %f: %s" % (seeing, indices)) 

128 return name + "_%d" % (indices[0],) 

129 

130 def getApertureResultName(self, seeing, radius, name=PLUGIN_NAME): 

131 """Return name for aperture measurement result 

132 

133 Parameters 

134 ---------- 

135 seeing : `float` 

136 The seeing value; it is required that the `ConvolvedFluxConfig.seeing` list 

137 include this value. 

138 radius : `float` 

139 The aperture radius. If this doesn't correspond to a value in the 

140 `ConvolvedFluxConfig.aperture.radii` then the returned name may not be useful. 

141 name : `str`, optional 

142 The name of the plugin. 

143 

144 Returns 

145 ------- 

146 resultName : `str` 

147 Result name for aperture measurement with nominated seeing and radius. 

148 """ 

149 baseName = self.getBaseNameForSeeing(seeing, name=name) 

150 return lsst.meas.base.CircularApertureFluxAlgorithm.makeFieldPrefix(baseName, radius) 

151 

152 def getKronResultName(self, seeing, name=PLUGIN_NAME): 

153 """Return name for Kron measurement result 

154 

155 Parameters 

156 ---------- 

157 seeing : `float` 

158 The seeing value; it is required that the `ConvolvedFluxConfig.seeing` list 

159 include this value. 

160 name : `str`, optional 

161 The name of the plugin. 

162 

163 Returns 

164 ------- 

165 resultName : `str` 

166 Result name for Kron measurement with nominated seeing. 

167 """ 

168 return self.getBaseNameForSeeing(seeing, name=name) + "_kron" 

169 

170 def getAllApertureResultNames(self, name=PLUGIN_NAME): 

171 """Return all names for aperture measurements 

172 

173 Parameters 

174 ---------- 

175 name : `str`, optional 

176 The name of the plugin. 

177 

178 Returns 

179 ------- 

180 results : `list` of `str` 

181 List of names for aperture measurements (for all seeings) 

182 """ 

183 return [lsst.meas.base.CircularApertureFluxAlgorithm.makeFieldPrefix(seeingName, radius) for 

184 seeingName in [name + "_%d" % (ii,) for ii in range(len(self.seeing))] for 

185 radius in self.aperture.radii] 

186 

187 def getAllKronResultNames(self, name=PLUGIN_NAME): 

188 """Return all names for Kron measurements 

189 

190 Parameters 

191 ---------- 

192 name : `str`, optional 

193 The name of the plugin. 

194 

195 Returns 

196 ------- 

197 results : `list` of `str` 

198 List of names for Kron measurements (for all seeings) 

199 """ 

200 return [name + "_%d_kron" % (ii,) for ii in range(len(self.seeing))] 

201 

202 def getAllResultNames(self, name=PLUGIN_NAME): 

203 """Return all names for measurements 

204 

205 Parameters 

206 ---------- 

207 name : `str`, optional 

208 The name of the plugin. 

209 

210 Returns 

211 ------- 

212 results : `list` of `str` 

213 List of names for measurements (for all seeings and apertures and Kron) 

214 """ 

215 return self.getAllApertureResultNames(name=name) + self.getAllKronResultNames(name=name) 

216 

217 

218class BaseConvolvedFluxPlugin(lsst.meas.base.BaseMeasurementPlugin): 

219 """Calculate aperture fluxes on images convolved to target seeing. 

220 

221 This measurement plugin convolves the image to match a target seeing 

222 and measures fluxes within circular apertures and within the Kron 

223 aperture (defined as a multiple of the Kron radius which is already 

224 available in the catalog). 

225 

226 Throughout, we assume a Gaussian PSF to simplify and optimise the 

227 convolution for speed. The results are therefore not exact, but should 

228 be good enough to be useful. 

229 

230 The measurements produced by this plugin are useful for: 

231 * Fiber mags: the flux within a circular aperture in a particular seeing 

232 can be used to calibrate fiber-fed spectroscopic observations. 

233 * Galaxy photometry: the flux within an aperture in common seeing can 

234 be used to measure good colors for an object without assuming a model. 

235 

236 The error handling is a bit different from most measurement plugins (which 

237 are content to fail anywhere and have the entire algorithm flagged as having 

238 failed), because we have multiple components (circular apertures and Kron) 

239 and we don't want the whole to fail because one component failed. Therefore, 

240 there's a few more try/except blocks than might be otherwise expected. 

241 """ 

242 

243 @classmethod 

244 def getExecutionOrder(cls): 

245 return KronFluxPlugin.getExecutionOrder() + 0.1 # Should run after Kron because we need the radius 

246 

247 def __init__(self, config, name, schema, metadata): 

248 """Ctor 

249 

250 Parameters 

251 ---------- 

252 config : `ConvolvedFluxConfig` 

253 Configuration for plugin. 

254 name : `str` 

255 Name of plugin (used as prefix for columns in schema). 

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

257 Catalog schema. 

258 metadata : `lsst.daf.base.PropertyList` 

259 Algorithm metadata to be recorded in the catalog header. 

260 """ 

261 lsst.meas.base.BaseMeasurementPlugin.__init__(self, config, name) 

262 self.seeingKey = schema.addField(name + "_seeing", type="F", 

263 doc="original seeing (Gaussian sigma) at position", 

264 units="pixel") 

265 self.data = [ConvolvedFluxData(self.config.getBaseNameForSeeing(seeing, name=name), schema, seeing, 

266 self.config, metadata) for seeing in self.config.seeing] 

267 

268 flagDefs = lsst.meas.base.FlagDefinitionList() 

269 flagDefs.addFailureFlag("error in running ConvolvedFluxPlugin") 

270 self.flagHandler = lsst.meas.base.FlagHandler.addFields(schema, name, flagDefs) 

271 if self.config.registerForApCorr: 

272 # Trigger aperture corrections for all flux measurements 

273 for apName in self.config.getAllApertureResultNames(name): 

274 lsst.meas.base.addApCorrName(apName) 

275 for kronName in self.config.getAllKronResultNames(name): 

276 lsst.meas.base.addApCorrName(kronName) 

277 

278 self.centroidExtractor = lsst.meas.base.SafeCentroidExtractor(schema, name) 

279 

280 def measure(self, measRecord, exposure): 

281 """Measure source on image 

282 

283 Parameters 

284 ---------- 

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

286 Record for source to be measured. 

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

288 Image to be measured. 

289 """ 

290 return self.measureForced(measRecord, exposure, measRecord, None) 

291 

292 def measureForced(self, measRecord, exposure, refRecord, refWcs): 

293 """Measure source on image in forced mode 

294 

295 Parameters 

296 ---------- 

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

298 Record for source to be measured. 

299 exposure : `lsst.afw.image.Exposure` 

300 Image to be measured. 

301 refRecord : `lsst.afw.table.SourceRecord` 

302 Record providing reference position and aperture. 

303 refWcs : `lsst.afw.geom.SkyWcs` or `None` 

304 Astrometric solution for reference, or `None` for no conversion 

305 from reference to measurement frame. 

306 """ 

307 psf = exposure.getPsf() 

308 if psf is None: 

309 raise lsst.meas.base.MeasurementError("No PSF in exposure") 

310 

311 refCenter = self.centroidExtractor(refRecord, self.flagHandler) 

312 

313 if refWcs is not None: 

314 measWcs = exposure.getWcs() 

315 if measWcs is None: 

316 raise lsst.meas.base.MeasurementError("No WCS in exposure") 

317 fullTransform = makeWcsPairTransform(refWcs, measWcs) 

318 transform = lsst.afw.geom.linearizeTransform(fullTransform, refCenter) 

319 else: 

320 transform = lsst.geom.AffineTransform() 

321 

322 kron = self.getKronAperture(refRecord, transform) 

323 

324 center = refCenter if transform is None else transform(refCenter) 

325 seeing = psf.computeShape(center).getDeterminantRadius() 

326 measRecord.set(self.seeingKey, seeing) 

327 

328 maxRadius = self.getMaxRadius(kron) 

329 for ii, target in enumerate(self.config.seeing): 

330 try: 

331 convolved = self.convolve(exposure, seeing, target/SIGMA_TO_FWHM, measRecord.getFootprint(), 

332 maxRadius) 

333 except (DeconvolutionError, RuntimeError): 

334 # Record the problem, but allow the measurement to run in case it's useful 

335 measRecord.set(self.data[ii].deconvKey, True) 

336 convolved = exposure 

337 self.measureAperture(measRecord, convolved, self.data[ii].aperture) 

338 if kron is not None: 

339 self.measureForcedKron(measRecord, self.data[ii].kronKeys, convolved.getMaskedImage(), kron) 

340 

341 def fail(self, measRecord, error=None): 

342 """Record failure 

343 

344 Called by the measurement framework when it catches an exception. 

345 

346 Parameters 

347 ---------- 

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

349 Record for source on which measurement failed. 

350 error : `Exception`, optional 

351 Error that occurred, or None. 

352 """ 

353 self.flagHandler.handleFailure(measRecord) 

354 

355 def getKronAperture(self, refRecord, transform): 

356 """Determine the Kron radius 

357 

358 Because we need to know the size of the area beforehand (we don't want to convolve 

359 the entire image just for this source), we are not measuring an independent Kron 

360 radius, but using the Kron radius that's already provided in the `refRecord` as 

361 `ConvolvedFluxConfig.kronRadiusName`. 

362 

363 Parameters 

364 ---------- 

365 refRecord : `lsst.afw.table.SourceRecord` 

366 Record for source defining Kron aperture. 

367 transform : `lsst.geom.AffineTransform` 

368 Transformation to apply to reference aperture. 

369 

370 Returns 

371 ------- 

372 aperture : `lsst.meas.extensions.photometryKron.KronAperture` 

373 Kron aperture. 

374 """ 

375 try: 

376 radius = refRecord.get(self.config.kronRadiusName) 

377 except KeyError: 

378 return None 

379 if not np.isfinite(radius): 

380 return None 

381 return KronAperture(refRecord, transform, radius) 

382 

383 def getMaxRadius(self, kron): 

384 """Determine the maximum radius we care about 

385 

386 Because we don't want to convolve the entire image just for this source, 

387 we determine the maximum radius we care about for this source and will 

388 convolve only that. 

389 

390 Parameters 

391 ---------- 

392 kron : `lsst.meas.extensions.photometryKron.KronAperture` or `None` 

393 Kron aperture, or `None` if unavailable. 

394 

395 Returns 

396 ------- 

397 maxRadius : `int` 

398 Maximum radius of interest. 

399 """ 

400 kronRadius = kron.getAxes().getDeterminantRadius() if kron is not None else 0.0 

401 return int(max(max(self.config.aperture.radii), self.config.kronRadiusForFlux*kronRadius) + 0.5) 

402 

403 def convolve(self, exposure, seeing, target, footprint, maxRadius): 

404 """Convolve image around source to target seeing 

405 

406 We also record the original seeing at the source position. 

407 

408 Because we don't want to convolve the entire image just for this source, 

409 we cut out an area corresponding to the source's footprint, grown by the 

410 radius provided by `maxRadius`. 

411 

412 We assume a Gaussian PSF to simplify and speed the convolution. 

413 The `seeing` and `target` may be either Gaussian sigma or FWHM, so long 

414 as they are the same. 

415 

416 Parameters 

417 ---------- 

418 exposure : `lsst.afw.image.Exposure` 

419 Image to convolve. 

420 seeing : `float` 

421 Current seeing, pixels. 

422 target : `float` 

423 Desired target seeing, pixels. 

424 footprint : `lsst.afw.detection.Footprint` 

425 Footprint for source. 

426 maxRadius : `int` 

427 Maximum radius required by measurement algorithms. 

428 

429 Returns 

430 ------- 

431 convExp : `lsst.afw.image.Exposure` 

432 Sub-image containing the source, convolved to the target seeing. 

433 

434 Raises 

435 ------ 

436 DeconvolutionError 

437 If the target seeing requires deconvolution. 

438 RuntimeError 

439 If the bounding box is too small after clipping. 

440 """ 

441 

442 if target < seeing: 

443 raise DeconvolutionError("Target seeing requires deconvolution") 

444 kernelSigma = math.sqrt(target*target - seeing*seeing) 

445 kernelRadius = int(self.config.kernelScale*kernelSigma + 0.5) 

446 kernelWidth = 2*kernelRadius + 1 

447 gauss = lsst.afw.math.GaussianFunction1D(kernelSigma) 

448 kernel = lsst.afw.math.SeparableKernel(kernelWidth, kernelWidth, gauss, gauss) 

449 

450 bbox = footprint.getBBox() 

451 bbox.grow(kernelRadius + maxRadius) # add an extra buffer? 

452 bbox.clip(exposure.getBBox()) 

453 if bbox.getWidth() < kernelWidth or bbox.getHeight() < kernelWidth: 

454 raise RuntimeError("Bounding box is too small following clipping") 

455 

456 image = exposure.getMaskedImage() 

457 subImage = image.Factory(image, bbox) 

458 convolved = image.Factory(bbox) 

459 lsst.afw.math.convolve(convolved, subImage, kernel, lsst.afw.math.ConvolutionControl(True, True)) 

460 

461 convExp = lsst.afw.image.makeExposure(convolved) 

462 convExp.setInfo(lsst.afw.image.ExposureInfo(exposure.getInfo())) 

463 

464 return convExp 

465 

466 def measureAperture(self, measRecord, exposure, aperturePhot): 

467 """Perform aperture photometry 

468 

469 Parameters 

470 ---------- 

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

472 Record for source to be measured. 

473 exposure : `lsst.afw.image.Exposure` 

474 Image to be measured. 

475 aperturePhot : `lsst.meas.base.CircularApertureFluxAlgorithm` 

476 Measurement plugin that will do the measurement. 

477 """ 

478 try: 

479 aperturePhot.measure(measRecord, exposure) 

480 except Exception: 

481 aperturePhot.fail(measRecord) 

482 

483 def measureForcedKron(self, measRecord, keys, image, aperture): 

484 """Measure forced Kron 

485 

486 Because we need to know the size of the area beforehand (we don't want to convolve 

487 the entire image just for this source), we are doing forced measurement using the 

488 Kron radius previously determined. 

489 

490 Parameters 

491 ---------- 

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

493 Record for source to be measured. 

494 keys : `lsst.pipe.base.Struct` 

495 Struct containing `result` (`lsst.meas.base.FluxResult`) and 

496 `flag` (`lsst.afw.table.Key_Flag`); provided by 

497 `ConvolvedFluxData.kronKeys`. 

498 image : `lsst.afw.image.MaskedImage` 

499 Image to be measured. 

500 aperture : `lsst.meas.extensions.photometryKron.KronAperture` 

501 Kron aperture to measure. 

502 """ 

503 measRecord.set(keys.flag, True) # failed unless we survive to switch this back 

504 if aperture is None: 

505 return # We've already flagged it, so just bail 

506 try: 

507 flux = aperture.measureFlux(image, self.config.kronRadiusForFlux, self.config.maxSincRadius) 

508 except Exception: 

509 return # We've already flagged it, so just bail 

510 measRecord.set(keys.result.getInstFlux(), flux[0]) 

511 measRecord.set(keys.result.getInstFluxErr(), flux[1]) 

512 measRecord.setFlag(keys.flag, bool(np.any(~np.isfinite(flux)))) 

513 

514 

515def wrapPlugin(Base, PluginClass=BaseConvolvedFluxPlugin, ConfigClass=BaseConvolvedFluxConfig, 

516 name=PLUGIN_NAME, factory=BaseConvolvedFluxPlugin): 

517 """Wrap plugin for use 

518 

519 A plugin has to inherit from a specific base class in order to be used 

520 in a particular context (e.g., single frame vs forced measurement). 

521 

522 Parameters 

523 ---------- 

524 Base : `type` 

525 Base class to give the plugin. 

526 PluginClass : `type` 

527 Plugin class to wrap. 

528 ConfigClass : `type` 

529 Configuration class; should subclass `lsst.pex.config.Config`. 

530 name : `str` 

531 Name of plugin. 

532 factory : callable 

533 Callable to create an instance of the `PluginClass`. 

534 

535 Returns 

536 ------- 

537 WrappedPlugin : `type` 

538 The wrapped plugin class (subclass of `Base`). 

539 WrappedConfig : `type` 

540 The wrapped plugin configuration (subclass of `Base.ConfigClass`). 

541 """ 

542 WrappedConfig = type("ConvolvedFlux" + Base.ConfigClass.__name__, (Base.ConfigClass, ConfigClass), {}) 

543 typeDict = dict(AlgClass=PluginClass, ConfigClass=WrappedConfig, factory=factory, 

544 getExecutionOrder=PluginClass.getExecutionOrder) 

545 WrappedPlugin = type("ConvolvedFlux" + Base.__name__, (Base,), typeDict) 

546 Base.registry.register(name, WrappedPlugin) 

547 return WrappedPlugin, WrappedConfig 

548 

549 

550def wrapPluginForced(Base, PluginClass=BaseConvolvedFluxPlugin, ConfigClass=BaseConvolvedFluxConfig, 

551 name=PLUGIN_NAME, factory=BaseConvolvedFluxPlugin): 

552 """Wrap plugin for use in forced measurement 

553 

554 A version of `wrapPlugin` that generates a `factory` suitable for 

555 forced measurement. This is important because the required signature 

556 for the factory in forced measurement includes a 'schemaMapper' instead 

557 of a 'schema'. 

558 

559 Parameters 

560 ---------- 

561 Base : `type` 

562 Base class to give the plugin. 

563 PluginClass : `type` 

564 Plugin class to wrap. 

565 ConfigClass : `type` 

566 Configuration class; should subclass `lsst.pex.config.Config`. 

567 name : `str` 

568 Name of plugin. 

569 factory : callable 

570 Callable to create an instance of the `PluginClass`. 

571 

572 Returns 

573 ------- 

574 WrappedPlugin : `type` 

575 The wrapped plugin class (subclass of `Base`). 

576 WrappedConfig : `type` 

577 The wrapped plugin configuration (subclass of `Base.ConfigClass`). 

578 """ 

579 

580 def forcedPluginFactory(name, config, schemaMapper, metadata): 

581 return factory(name, config, schemaMapper.editOutputSchema(), metadata) 

582 return wrapPlugin(Base, PluginClass=PluginClass, ConfigClass=ConfigClass, name=name, 

583 factory=staticmethod(forcedPluginFactory)) 

584 

585 

586SingleFrameConvolvedFluxPlugin, SingleFrameConvolvedFluxConfig = wrapPlugin(WrappedSingleFramePlugin) 

587ForcedConvolvedFluxPlugin, ForcedConvolvedFluxConfig = wrapPluginForced(WrappedForcedPlugin)