Coverage for python/lsst/ip/diffim/subtractImages.py: 23%

193 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-11 03:35 -0800

1# This file is part of ip_diffim. 

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 

22import numpy as np 

23 

24import lsst.afw.image 

25import lsst.afw.math 

26import lsst.geom 

27from lsst.ip.diffim.utils import getPsfFwhm 

28from lsst.meas.algorithms import ScaleVarianceTask 

29import lsst.pex.config 

30import lsst.pipe.base 

31from lsst.pipe.base import connectionTypes 

32from . import MakeKernelTask, DecorrelateALKernelTask 

33from lsst.utils.timer import timeMethod 

34 

35__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask"] 

36 

37_dimensions = ("instrument", "visit", "detector") 

38_defaultTemplates = {"coaddName": "deep", "fakesType": ""} 

39 

40 

41class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections, 

42 dimensions=_dimensions, 

43 defaultTemplates=_defaultTemplates): 

44 template = connectionTypes.Input( 

45 doc="Input warped template to subtract.", 

46 dimensions=("instrument", "visit", "detector"), 

47 storageClass="ExposureF", 

48 name="{fakesType}{coaddName}Diff_templateExp" 

49 ) 

50 science = connectionTypes.Input( 

51 doc="Input science exposure to subtract from.", 

52 dimensions=("instrument", "visit", "detector"), 

53 storageClass="ExposureF", 

54 name="{fakesType}calexp" 

55 ) 

56 sources = connectionTypes.Input( 

57 doc="Sources measured on the science exposure; " 

58 "used to select sources for making the matching kernel.", 

59 dimensions=("instrument", "visit", "detector"), 

60 storageClass="SourceCatalog", 

61 name="{fakesType}src" 

62 ) 

63 finalizedPsfApCorrCatalog = connectionTypes.Input( 

64 doc=("Per-visit finalized psf models and aperture correction maps. " 

65 "These catalogs use the detector id for the catalog id, " 

66 "sorted on id for fast lookup."), 

67 dimensions=("instrument", "visit"), 

68 storageClass="ExposureCatalog", 

69 name="finalized_psf_ap_corr_catalog", 

70 ) 

71 

72 

73class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections, 

74 dimensions=_dimensions, 

75 defaultTemplates=_defaultTemplates): 

76 difference = connectionTypes.Output( 

77 doc="Result of subtracting convolved template from science image.", 

78 dimensions=("instrument", "visit", "detector"), 

79 storageClass="ExposureF", 

80 name="{fakesType}{coaddName}Diff_differenceTempExp", 

81 ) 

82 matchedTemplate = connectionTypes.Output( 

83 doc="Warped and PSF-matched template used to create `subtractedExposure`.", 

84 dimensions=("instrument", "visit", "detector"), 

85 storageClass="ExposureF", 

86 name="{fakesType}{coaddName}Diff_matchedExp", 

87 ) 

88 

89 

90class AlardLuptonSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections): 

91 

92 def __init__(self, *, config=None): 

93 super().__init__(config=config) 

94 if not config.doApplyFinalizedPsf: 

95 self.inputs.remove("finalizedPsfApCorrCatalog") 

96 

97 

98class AlardLuptonSubtractConfig(lsst.pipe.base.PipelineTaskConfig, 

99 pipelineConnections=AlardLuptonSubtractConnections): 

100 mode = lsst.pex.config.ChoiceField( 

101 dtype=str, 

102 default="convolveTemplate", 

103 allowed={"auto": "Choose which image to convolve at runtime.", 

104 "convolveScience": "Only convolve the science image.", 

105 "convolveTemplate": "Only convolve the template image."}, 

106 doc="Choose which image to convolve at runtime, or require that a specific image is convolved." 

107 ) 

108 makeKernel = lsst.pex.config.ConfigurableField( 

109 target=MakeKernelTask, 

110 doc="Task to construct a matching kernel for convolution.", 

111 ) 

112 doDecorrelation = lsst.pex.config.Field( 

113 dtype=bool, 

114 default=True, 

115 doc="Perform diffim decorrelation to undo pixel correlation due to A&L " 

116 "kernel convolution? If True, also update the diffim PSF." 

117 ) 

118 decorrelate = lsst.pex.config.ConfigurableField( 

119 target=DecorrelateALKernelTask, 

120 doc="Task to decorrelate the image difference.", 

121 ) 

122 requiredTemplateFraction = lsst.pex.config.Field( 

123 dtype=float, 

124 default=0.1, 

125 doc="Abort task if template covers less than this fraction of pixels." 

126 " Setting to 0 will always attempt image subtraction." 

127 ) 

128 doScaleVariance = lsst.pex.config.Field( 

129 dtype=bool, 

130 default=True, 

131 doc="Scale variance of the image difference?" 

132 ) 

133 scaleVariance = lsst.pex.config.ConfigurableField( 

134 target=ScaleVarianceTask, 

135 doc="Subtask to rescale the variance of the template to the statistically expected level." 

136 ) 

137 doSubtractBackground = lsst.pex.config.Field( 

138 doc="Subtract the background fit when solving the kernel?", 

139 dtype=bool, 

140 default=True, 

141 ) 

142 doApplyFinalizedPsf = lsst.pex.config.Field( 

143 doc="Replace science Exposure's psf and aperture correction map" 

144 " with those in finalizedPsfApCorrCatalog.", 

145 dtype=bool, 

146 default=False, 

147 ) 

148 detectionThreshold = lsst.pex.config.Field( 

149 dtype=float, 

150 default=10, 

151 doc="Minimum signal to noise ration of detected sources " 

152 "to use for calculating the PSF matching kernel." 

153 ) 

154 badSourceFlags = lsst.pex.config.ListField( 

155 dtype=str, 

156 doc="Flags that, if set, the associated source should not " 

157 "be used to determine the PSF matching kernel.", 

158 default=("sky_source", "slot_Centroid_flag", 

159 "slot_ApFlux_flag", "slot_PsfFlux_flag", ), 

160 ) 

161 

162 def setDefaults(self): 

163 self.makeKernel.kernel.name = "AL" 

164 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground 

165 self.makeKernel.kernel.active.spatialKernelOrder = 1 

166 self.makeKernel.kernel.active.spatialBgOrder = 2 

167 

168 

169class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask): 

170 """Compute the image difference of a science and template image using 

171 the Alard & Lupton (1998) algorithm. 

172 """ 

173 ConfigClass = AlardLuptonSubtractConfig 

174 _DefaultName = "alardLuptonSubtract" 

175 

176 def __init__(self, **kwargs): 

177 super().__init__(**kwargs) 

178 self.makeSubtask("decorrelate") 

179 self.makeSubtask("makeKernel") 

180 if self.config.doScaleVariance: 

181 self.makeSubtask("scaleVariance") 

182 

183 self.convolutionControl = lsst.afw.math.ConvolutionControl() 

184 # Normalization is an extra, unnecessary, calculation and will result 

185 # in mis-subtraction of the images if there are calibration errors. 

186 self.convolutionControl.setDoNormalize(False) 

187 self.convolutionControl.setDoCopyEdge(True) 

188 

189 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog): 

190 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.". 

191 

192 Parameters 

193 ---------- 

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

195 Input exposure to adjust calibrations. 

196 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog` 

197 Exposure catalog with finalized psf models and aperture correction 

198 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses 

199 the detector id for the catalog id, sorted on id for fast lookup. 

200 

201 Returns 

202 ------- 

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

204 Exposure with adjusted calibrations. 

205 """ 

206 detectorId = exposure.info.getDetector().getId() 

207 

208 row = finalizedPsfApCorrCatalog.find(detectorId) 

209 if row is None: 

210 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; " 

211 "Using original psf.", detectorId) 

212 else: 

213 psf = row.getPsf() 

214 apCorrMap = row.getApCorrMap() 

215 if psf is None: 

216 self.log.warning("Detector id %s has None for psf in " 

217 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.", 

218 detectorId) 

219 elif apCorrMap is None: 

220 self.log.warning("Detector id %s has None for apCorrMap in " 

221 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.", 

222 detectorId) 

223 else: 

224 exposure.setPsf(psf) 

225 exposure.info.setApCorrMap(apCorrMap) 

226 

227 return exposure 

228 

229 @timeMethod 

230 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None): 

231 """PSF match, subtract, and decorrelate two images. 

232 

233 Parameters 

234 ---------- 

235 template : `lsst.afw.image.ExposureF` 

236 Template exposure, warped to match the science exposure. 

237 science : `lsst.afw.image.ExposureF` 

238 Science exposure to subtract from the template. 

239 sources : `lsst.afw.table.SourceCatalog` 

240 Identified sources on the science exposure. This catalog is used to 

241 select sources in order to perform the AL PSF matching on stamp 

242 images around them. 

243 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional 

244 Exposure catalog with finalized psf models and aperture correction 

245 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses 

246 the detector id for the catalog id, sorted on id for fast lookup. 

247 

248 Returns 

249 ------- 

250 results : `lsst.pipe.base.Struct` 

251 ``difference`` : `lsst.afw.image.ExposureF` 

252 Result of subtracting template and science. 

253 ``matchedTemplate`` : `lsst.afw.image.ExposureF` 

254 Warped and PSF-matched template exposure. 

255 ``backgroundModel`` : `lsst.afw.math.Function2D` 

256 Background model that was fit while solving for the PSF-matching kernel 

257 ``psfMatchingKernel`` : `lsst.afw.math.Kernel` 

258 Kernel used to PSF-match the convolved image. 

259 

260 Raises 

261 ------ 

262 RuntimeError 

263 If an unsupported convolution mode is supplied. 

264 RuntimeError 

265 If there are too few sources to calculate the PSF matching kernel. 

266 lsst.pipe.base.NoWorkFound 

267 Raised if fraction of good pixels, defined as not having NO_DATA 

268 set, is less then the configured requiredTemplateFraction 

269 """ 

270 self._validateExposures(template, science) 

271 if self.config.doApplyFinalizedPsf: 

272 self._applyExternalCalibrations(science, 

273 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog) 

274 checkTemplateIsSufficient(template, self.log, 

275 requiredTemplateFraction=self.config.requiredTemplateFraction) 

276 sciencePsfSize = getPsfFwhm(science.psf) 

277 templatePsfSize = getPsfFwhm(template.psf) 

278 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize) 

279 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize) 

280 if self.config.mode == "auto": 

281 convolveTemplate = _shapeTest(template.psf, science.psf) 

282 if convolveTemplate: 

283 if sciencePsfSize < templatePsfSize: 

284 self.log.info("Average template PSF size is greater, " 

285 "but science PSF greater in one dimension: convolving template image.") 

286 else: 

287 self.log.info("Science PSF size is greater: convolving template image.") 

288 else: 

289 self.log.info("Template PSF size is greater: convolving science image.") 

290 elif self.config.mode == "convolveTemplate": 

291 self.log.info("`convolveTemplate` is set: convolving template image.") 

292 convolveTemplate = True 

293 elif self.config.mode == "convolveScience": 

294 self.log.info("`convolveScience` is set: convolving science image.") 

295 convolveTemplate = False 

296 else: 

297 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode) 

298 # put the template on the same photometric scale as the science image 

299 photoCalib = template.getPhotoCalib() 

300 self.log.info("Applying photometric calibration to template: %f", photoCalib.getCalibrationMean()) 

301 template.maskedImage = photoCalib.calibrateImage(template.maskedImage) 

302 

303 if self.config.doScaleVariance: 

304 # Scale the variance of the template and science images before 

305 # convolution, subtraction, or decorrelation so that they have the 

306 # correct ratio. 

307 templateVarFactor = self.scaleVariance.run(template.maskedImage) 

308 sciVarFactor = self.scaleVariance.run(science.maskedImage) 

309 self.log.info("Template variance scaling factor: %.2f", templateVarFactor) 

310 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor) 

311 self.log.info("Science variance scaling factor: %.2f", sciVarFactor) 

312 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor) 

313 

314 selectSources = self._sourceSelector(sources) 

315 self.log.info("%i sources used out of %i from the input catalog", len(selectSources), len(sources)) 

316 if len(selectSources) < self.config.makeKernel.nStarPerCell: 

317 self.log.warning("Too few sources to calculate the PSF matching kernel: " 

318 "%i selected but %i needed for the calculation.", 

319 len(selectSources), self.config.makeKernel.nStarPerCell) 

320 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.") 

321 if convolveTemplate: 

322 subtractResults = self.runConvolveTemplate(template, science, selectSources) 

323 else: 

324 subtractResults = self.runConvolveScience(template, science, selectSources) 

325 

326 return subtractResults 

327 

328 def runConvolveTemplate(self, template, science, selectSources): 

329 """Convolve the template image with a PSF-matching kernel and subtract 

330 from the science image. 

331 

332 Parameters 

333 ---------- 

334 template : `lsst.afw.image.ExposureF` 

335 Template exposure, warped to match the science exposure. 

336 science : `lsst.afw.image.ExposureF` 

337 Science exposure to subtract from the template. 

338 selectSources : `lsst.afw.table.SourceCatalog` 

339 Identified sources on the science exposure. This catalog is used to 

340 select sources in order to perform the AL PSF matching on stamp 

341 images around them. 

342 

343 Returns 

344 ------- 

345 results : `lsst.pipe.base.Struct` 

346 

347 ``difference`` : `lsst.afw.image.ExposureF` 

348 Result of subtracting template and science. 

349 ``matchedTemplate`` : `lsst.afw.image.ExposureF` 

350 Warped and PSF-matched template exposure. 

351 ``backgroundModel`` : `lsst.afw.math.Function2D` 

352 Background model that was fit while solving for the PSF-matching kernel 

353 ``psfMatchingKernel`` : `lsst.afw.math.Kernel` 

354 Kernel used to PSF-match the template to the science image. 

355 """ 

356 kernelSources = self.makeKernel.selectKernelSources(template, science, 

357 candidateList=selectSources, 

358 preconvolved=False) 

359 kernelResult = self.makeKernel.run(template, science, kernelSources, 

360 preconvolved=False) 

361 

362 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel, 

363 self.convolutionControl, 

364 bbox=science.getBBox(), 

365 psf=science.psf, 

366 photoCalib=science.getPhotoCalib()) 

367 difference = _subtractImages(science, matchedTemplate, 

368 backgroundModel=(kernelResult.backgroundModel 

369 if self.config.doSubtractBackground else None)) 

370 correctedExposure = self.finalize(template, science, difference, kernelResult.psfMatchingKernel, 

371 templateMatched=True) 

372 

373 return lsst.pipe.base.Struct(difference=correctedExposure, 

374 matchedTemplate=matchedTemplate, 

375 matchedScience=science, 

376 backgroundModel=kernelResult.backgroundModel, 

377 psfMatchingKernel=kernelResult.psfMatchingKernel) 

378 

379 def runConvolveScience(self, template, science, selectSources): 

380 """Convolve the science image with a PSF-matching kernel and subtract the template image. 

381 

382 Parameters 

383 ---------- 

384 template : `lsst.afw.image.ExposureF` 

385 Template exposure, warped to match the science exposure. 

386 science : `lsst.afw.image.ExposureF` 

387 Science exposure to subtract from the template. 

388 selectSources : `lsst.afw.table.SourceCatalog` 

389 Identified sources on the science exposure. This catalog is used to 

390 select sources in order to perform the AL PSF matching on stamp 

391 images around them. 

392 

393 Returns 

394 ------- 

395 results : `lsst.pipe.base.Struct` 

396 

397 ``difference`` : `lsst.afw.image.ExposureF` 

398 Result of subtracting template and science. 

399 ``matchedTemplate`` : `lsst.afw.image.ExposureF` 

400 Warped template exposure. Note that in this case, the template 

401 is not PSF-matched to the science image. 

402 ``backgroundModel`` : `lsst.afw.math.Function2D` 

403 Background model that was fit while solving for the PSF-matching kernel 

404 ``psfMatchingKernel`` : `lsst.afw.math.Kernel` 

405 Kernel used to PSF-match the science image to the template. 

406 """ 

407 kernelSources = self.makeKernel.selectKernelSources(science, template, 

408 candidateList=selectSources, 

409 preconvolved=False) 

410 kernelResult = self.makeKernel.run(science, template, kernelSources, 

411 preconvolved=False) 

412 modelParams = kernelResult.backgroundModel.getParameters() 

413 # We must invert the background model if the matching kernel is solved for the science image. 

414 kernelResult.backgroundModel.setParameters([-p for p in modelParams]) 

415 

416 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions()) 

417 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False) 

418 

419 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel, 

420 self.convolutionControl, 

421 psf=template.psf) 

422 

423 # Place back on native photometric scale 

424 matchedScience.maskedImage /= norm 

425 matchedTemplate = template.clone()[science.getBBox()] 

426 matchedTemplate.maskedImage /= norm 

427 matchedTemplate.setPhotoCalib(science.getPhotoCalib()) 

428 

429 difference = _subtractImages(matchedScience, matchedTemplate, 

430 backgroundModel=(kernelResult.backgroundModel 

431 if self.config.doSubtractBackground else None)) 

432 

433 correctedExposure = self.finalize(template, science, difference, kernelResult.psfMatchingKernel, 

434 templateMatched=False) 

435 

436 return lsst.pipe.base.Struct(difference=correctedExposure, 

437 matchedTemplate=matchedTemplate, 

438 matchedScience=matchedScience, 

439 backgroundModel=kernelResult.backgroundModel, 

440 psfMatchingKernel=kernelResult.psfMatchingKernel,) 

441 

442 def finalize(self, template, science, difference, kernel, 

443 templateMatched=True, 

444 preConvMode=False, 

445 preConvKernel=None, 

446 spatiallyVarying=False): 

447 """Decorrelate the difference image to undo the noise correlations 

448 caused by convolution. 

449 

450 Parameters 

451 ---------- 

452 template : `lsst.afw.image.ExposureF` 

453 Template exposure, warped to match the science exposure. 

454 science : `lsst.afw.image.ExposureF` 

455 Science exposure to subtract from the template. 

456 difference : `lsst.afw.image.ExposureF` 

457 Result of subtracting template and science. 

458 kernel : `lsst.afw.math.Kernel` 

459 An (optionally spatially-varying) PSF matching kernel 

460 templateMatched : `bool`, optional 

461 Was the template PSF-matched to the science image? 

462 preConvMode : `bool`, optional 

463 Was the science image preconvolved with its own PSF 

464 before PSF matching the template? 

465 preConvKernel : `lsst.afw.detection.Psf`, optional 

466 If not `None`, then the science image was pre-convolved with 

467 (the reflection of) this kernel. Must be normalized to sum to 1. 

468 spatiallyVarying : `bool`, optional 

469 Compute the decorrelation kernel spatially varying across the image? 

470 

471 Returns 

472 ------- 

473 correctedExposure : `lsst.afw.image.ExposureF` 

474 The decorrelated image difference. 

475 """ 

476 # Erase existing detection mask planes. 

477 # We don't want the detection mask from the science image 

478 mask = difference.mask 

479 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")) 

480 

481 if self.config.doDecorrelation: 

482 self.log.info("Decorrelating image difference.") 

483 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel, 

484 templateMatched=templateMatched, 

485 preConvMode=preConvMode, 

486 preConvKernel=preConvKernel, 

487 spatiallyVarying=spatiallyVarying).correctedExposure 

488 else: 

489 self.log.info("NOT decorrelating image difference.") 

490 correctedExposure = difference 

491 return correctedExposure 

492 

493 @staticmethod 

494 def _validateExposures(template, science): 

495 """Check that the WCS of the two Exposures match, and the template bbox 

496 contains the science bbox. 

497 

498 Parameters 

499 ---------- 

500 template : `lsst.afw.image.ExposureF` 

501 Template exposure, warped to match the science exposure. 

502 science : `lsst.afw.image.ExposureF` 

503 Science exposure to subtract from the template. 

504 

505 Raises 

506 ------ 

507 AssertionError 

508 Raised if the WCS of the template is not equal to the science WCS, 

509 or if the science image is not fully contained in the template 

510 bounding box. 

511 """ 

512 assert template.wcs == science.wcs,\ 

513 "Template and science exposure WCS are not identical." 

514 templateBBox = template.getBBox() 

515 scienceBBox = science.getBBox() 

516 

517 assert templateBBox.contains(scienceBBox),\ 

518 "Template bbox does not contain all of the science image." 

519 

520 @staticmethod 

521 def _convolveExposure(exposure, kernel, convolutionControl, 

522 bbox=None, 

523 psf=None, 

524 photoCalib=None): 

525 """Convolve an exposure with the given kernel. 

526 

527 Parameters 

528 ---------- 

529 exposure : `lsst.afw.Exposure` 

530 exposure to convolve. 

531 kernel : `lsst.afw.math.LinearCombinationKernel` 

532 PSF matching kernel computed in the ``makeKernel`` subtask. 

533 convolutionControl : `lsst.afw.math.ConvolutionControl` 

534 Configuration for convolve algorithm. 

535 bbox : `lsst.geom.Box2I`, optional 

536 Bounding box to trim the convolved exposure to. 

537 psf : `lsst.afw.detection.Psf`, optional 

538 Point spread function (PSF) to set for the convolved exposure. 

539 photoCalib : `lsst.afw.image.PhotoCalib`, optional 

540 Photometric calibration of the convolved exposure. 

541 

542 Returns 

543 ------- 

544 convolvedExp : `lsst.afw.Exposure` 

545 The convolved image. 

546 """ 

547 convolvedExposure = exposure.clone() 

548 if psf is not None: 

549 convolvedExposure.setPsf(psf) 

550 if photoCalib is not None: 

551 convolvedExposure.setPhotoCalib(photoCalib) 

552 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox()) 

553 lsst.afw.math.convolve(convolvedImage, exposure.maskedImage, kernel, convolutionControl) 

554 convolvedExposure.setMaskedImage(convolvedImage) 

555 if bbox is None: 

556 return convolvedExposure 

557 else: 

558 return convolvedExposure[bbox] 

559 

560 def _sourceSelector(self, sources): 

561 """Select sources from a catalog that meet the selection criteria. 

562 

563 Parameters 

564 ---------- 

565 sources : `lsst.afw.table.SourceCatalog` 

566 Input source catalog to select sources from. 

567 

568 Returns 

569 ------- 

570 `lsst.afw.table.SourceCatalog` 

571 The source catalog filtered to include only the selected sources. 

572 """ 

573 flags = [True, ]*len(sources) 

574 for flag in self.config.badSourceFlags: 

575 try: 

576 flags *= ~sources[flag] 

577 except Exception as e: 

578 self.log.warning("Could not apply source flag: %s", e) 

579 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold 

580 flags *= sToNFlag 

581 selectSources = sources[flags] 

582 

583 return selectSources.copy(deep=True) 

584 

585 

586def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.): 

587 """Raise NoWorkFound if template coverage < requiredTemplateFraction 

588 

589 Parameters 

590 ---------- 

591 templateExposure : `lsst.afw.image.ExposureF` 

592 The template exposure to check 

593 logger : `lsst.log.Log` 

594 Logger for printing output. 

595 requiredTemplateFraction : `float`, optional 

596 Fraction of pixels of the science image required to have coverage 

597 in the template. 

598 

599 Raises 

600 ------ 

601 lsst.pipe.base.NoWorkFound 

602 Raised if fraction of good pixels, defined as not having NO_DATA 

603 set, is less then the configured requiredTemplateFraction 

604 """ 

605 # Count the number of pixels with the NO_DATA mask bit set 

606 # counting NaN pixels is insufficient because pixels without data are often intepolated over) 

607 pixNoData = np.count_nonzero(templateExposure.mask.array 

608 & templateExposure.mask.getPlaneBitMask('NO_DATA')) 

609 pixGood = templateExposure.getBBox().getArea() - pixNoData 

610 logger.info("template has %d good pixels (%.1f%%)", pixGood, 

611 100*pixGood/templateExposure.getBBox().getArea()) 

612 

613 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction: 

614 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. " 

615 "To force subtraction, set config requiredTemplateFraction=0." % ( 

616 100*pixGood/templateExposure.getBBox().getArea(), 

617 100*requiredTemplateFraction)) 

618 raise lsst.pipe.base.NoWorkFound(message) 

619 

620 

621def _subtractImages(science, template, backgroundModel=None): 

622 """Subtract template from science, propagating relevant metadata. 

623 

624 Parameters 

625 ---------- 

626 science : `lsst.afw.Exposure` 

627 The input science image. 

628 template : `lsst.afw.Exposure` 

629 The template to subtract from the science image. 

630 backgroundModel : `lsst.afw.MaskedImage`, optional 

631 Differential background model 

632 

633 Returns 

634 ------- 

635 difference : `lsst.afw.Exposure` 

636 The subtracted image. 

637 """ 

638 difference = science.clone() 

639 if backgroundModel is not None: 

640 difference.maskedImage -= backgroundModel 

641 difference.maskedImage -= template.maskedImage 

642 return difference 

643 

644 

645def _shapeTest(psf1, psf2): 

646 """Determine whether psf1 is narrower in either dimension than psf2. 

647 

648 Parameters 

649 ---------- 

650 psf1 : `lsst.afw.detection.Psf` 

651 Reference point spread function (PSF) to evaluate. 

652 psf2 : `lsst.afw.detection.Psf` 

653 Candidate point spread function (PSF) to evaluate. 

654 

655 Returns 

656 ------- 

657 `bool` 

658 Returns True if psf1 is narrower than psf2 in either dimension. 

659 """ 

660 shape1 = getPsfFwhm(psf1, average=False) 

661 shape2 = getPsfFwhm(psf2, average=False) 

662 xTest = shape1[0] < shape2[0] 

663 yTest = shape1[1] < shape2[1] 

664 return xTest | yTest