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

209 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-23 11:08 +0000

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 forceCompatibility = lsst.pex.config.Field( 

163 dtype=bool, 

164 default=False, 

165 doc="Set up and run diffim using settings that ensure the results" 

166 "are compatible with the old version in pipe_tasks.", 

167 deprecated="This option is only for backwards compatibility purposes" 

168 " and will be removed after v24.", 

169 ) 

170 

171 def setDefaults(self): 

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

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

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

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

176 

177 def validate(self): 

178 if self.forceCompatibility and not (self.mode == "convolveTemplate"): 

179 msg = f"forceCompatibility=True requires mode='convolveTemplate', but mode was '{self.mode}'." 

180 raise lsst.pex.config.FieldValidationError(AlardLuptonSubtractConfig.forceCompatibility, 

181 self, msg) 

182 

183 

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

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

186 the Alard & Lupton (1998) algorithm. 

187 """ 

188 ConfigClass = AlardLuptonSubtractConfig 

189 _DefaultName = "alardLuptonSubtract" 

190 

191 def __init__(self, **kwargs): 

192 super().__init__(**kwargs) 

193 self.makeSubtask("decorrelate") 

194 self.makeSubtask("makeKernel") 

195 if self.config.doScaleVariance: 

196 self.makeSubtask("scaleVariance") 

197 

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

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

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

201 self.convolutionControl.setDoNormalize(False) 

202 self.convolutionControl.setDoCopyEdge(True) 

203 

204 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog): 

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

206 

207 Parameters 

208 ---------- 

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

210 Input exposure to adjust calibrations. 

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

212 Exposure catalog with finalized psf models and aperture correction 

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

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

215 

216 Returns 

217 ------- 

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

219 Exposure with adjusted calibrations. 

220 """ 

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

222 

223 row = finalizedPsfApCorrCatalog.find(detectorId) 

224 if row is None: 

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

226 "Using original psf.", detectorId) 

227 else: 

228 psf = row.getPsf() 

229 apCorrMap = row.getApCorrMap() 

230 if psf is None: 

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

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

233 detectorId) 

234 elif apCorrMap is None: 

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

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

237 detectorId) 

238 else: 

239 exposure.setPsf(psf) 

240 exposure.info.setApCorrMap(apCorrMap) 

241 

242 return exposure 

243 

244 @timeMethod 

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

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

247 

248 Parameters 

249 ---------- 

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

251 Template exposure, warped to match the science exposure. 

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

253 Science exposure to subtract from the template. 

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

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

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

257 images around them. 

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

259 Exposure catalog with finalized psf models and aperture correction 

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

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

262 

263 Returns 

264 ------- 

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

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

267 Result of subtracting template and science. 

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

269 Warped and PSF-matched template exposure. 

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

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

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

273 Kernel used to PSF-match the convolved image. 

274 

275 Raises 

276 ------ 

277 RuntimeError 

278 If an unsupported convolution mode is supplied. 

279 RuntimeError 

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

281 lsst.pipe.base.NoWorkFound 

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

283 set, is less then the configured requiredTemplateFraction 

284 """ 

285 self._validateExposures(template, science) 

286 if self.config.doApplyFinalizedPsf: 

287 self._applyExternalCalibrations(science, 

288 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog) 

289 checkTemplateIsSufficient(template, self.log, 

290 requiredTemplateFraction=self.config.requiredTemplateFraction) 

291 if self.config.forceCompatibility: 

292 # Compatibility option to maintain old functionality 

293 # This should be removed in the future! 

294 self.log.warning("Running with `config.forceCompatibility=True`") 

295 sources = None 

296 sciencePsfSize = getPsfFwhm(science.psf) 

297 templatePsfSize = getPsfFwhm(template.psf) 

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

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

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

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

302 if convolveTemplate: 

303 if sciencePsfSize < templatePsfSize: 

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

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

306 else: 

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

308 else: 

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

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

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

312 convolveTemplate = True 

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

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

315 convolveTemplate = False 

316 else: 

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

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

319 photoCalib = template.getPhotoCalib() 

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

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

322 

323 if self.config.doScaleVariance and not self.config.forceCompatibility: 

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

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

326 # correct ratio. 

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

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

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

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

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

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

333 

334 selectSources = self._sourceSelector(sources) 

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

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

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

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

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

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

341 if convolveTemplate: 

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

343 else: 

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

345 

346 if self.config.doScaleVariance and self.config.forceCompatibility: 

347 # The old behavior scaled the variance of the final image difference. 

348 diffimVarFactor = self.scaleVariance.run(subtractResults.difference.maskedImage) 

349 self.log.info("Diffim variance scaling factor: %.2f", diffimVarFactor) 

350 self.metadata.add("scaleDiffimVarianceFactor", diffimVarFactor) 

351 

352 return subtractResults 

353 

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

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

356 from the science image. 

357 

358 Parameters 

359 ---------- 

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

361 Template exposure, warped to match the science exposure. 

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

363 Science exposure to subtract from the template. 

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

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

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

367 images around them. 

368 

369 Returns 

370 ------- 

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

372 

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

374 Result of subtracting template and science. 

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

376 Warped and PSF-matched template exposure. 

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

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

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

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

381 """ 

382 if self.config.forceCompatibility: 

383 # Compatibility option to maintain old behavior 

384 # This should be removed in the future! 

385 template = template[science.getBBox()] 

386 

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

388 candidateList=selectSources, 

389 preconvolved=False) 

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

391 preconvolved=False) 

392 

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

394 self.convolutionControl, 

395 bbox=science.getBBox(), 

396 psf=science.psf, 

397 photoCalib=science.getPhotoCalib()) 

398 difference = _subtractImages(science, matchedTemplate, 

399 backgroundModel=(kernelResult.backgroundModel 

400 if self.config.doSubtractBackground else None)) 

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

402 templateMatched=True) 

403 

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

405 matchedTemplate=matchedTemplate, 

406 matchedScience=science, 

407 backgroundModel=kernelResult.backgroundModel, 

408 psfMatchingKernel=kernelResult.psfMatchingKernel) 

409 

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

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

412 

413 Parameters 

414 ---------- 

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

416 Template exposure, warped to match the science exposure. 

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

418 Science exposure to subtract from the template. 

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

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

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

422 images around them. 

423 

424 Returns 

425 ------- 

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

427 

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

429 Result of subtracting template and science. 

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

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

432 is not PSF-matched to the science image. 

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

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

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

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

437 """ 

438 if self.config.forceCompatibility: 

439 # Compatibility option to maintain old behavior 

440 # This should be removed in the future! 

441 template = template[science.getBBox()] 

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

443 candidateList=selectSources, 

444 preconvolved=False) 

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

446 preconvolved=False) 

447 modelParams = kernelResult.backgroundModel.getParameters() 

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

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

450 

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

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

453 

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

455 self.convolutionControl, 

456 psf=template.psf) 

457 

458 # Place back on native photometric scale 

459 matchedScience.maskedImage /= norm 

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

461 matchedTemplate.maskedImage /= norm 

462 matchedTemplate.setPhotoCalib(science.getPhotoCalib()) 

463 

464 difference = _subtractImages(matchedScience, matchedTemplate, 

465 backgroundModel=(kernelResult.backgroundModel 

466 if self.config.doSubtractBackground else None)) 

467 

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

469 templateMatched=False) 

470 

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

472 matchedTemplate=matchedTemplate, 

473 matchedScience=matchedScience, 

474 backgroundModel=kernelResult.backgroundModel, 

475 psfMatchingKernel=kernelResult.psfMatchingKernel,) 

476 

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

478 templateMatched=True, 

479 preConvMode=False, 

480 preConvKernel=None, 

481 spatiallyVarying=False): 

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

483 caused by convolution. 

484 

485 Parameters 

486 ---------- 

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

488 Template exposure, warped to match the science exposure. 

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

490 Science exposure to subtract from the template. 

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

492 Result of subtracting template and science. 

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

494 An (optionally spatially-varying) PSF matching kernel 

495 templateMatched : `bool`, optional 

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

497 preConvMode : `bool`, optional 

498 Was the science image preconvolved with its own PSF 

499 before PSF matching the template? 

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

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

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

503 spatiallyVarying : `bool`, optional 

504 Compute the decorrelation kernel spatially varying across the image? 

505 

506 Returns 

507 ------- 

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

509 The decorrelated image difference. 

510 """ 

511 # Erase existing detection mask planes. 

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

513 mask = difference.mask 

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

515 

516 if self.config.doDecorrelation: 

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

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

519 templateMatched=templateMatched, 

520 preConvMode=preConvMode, 

521 preConvKernel=preConvKernel, 

522 spatiallyVarying=spatiallyVarying).correctedExposure 

523 else: 

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

525 correctedExposure = difference 

526 return correctedExposure 

527 

528 @staticmethod 

529 def _validateExposures(template, science): 

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

531 contains the science bbox. 

532 

533 Parameters 

534 ---------- 

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

536 Template exposure, warped to match the science exposure. 

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

538 Science exposure to subtract from the template. 

539 

540 Raises 

541 ------ 

542 AssertionError 

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

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

545 bounding box. 

546 """ 

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

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

549 templateBBox = template.getBBox() 

550 scienceBBox = science.getBBox() 

551 

552 assert templateBBox.contains(scienceBBox),\ 

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

554 

555 @staticmethod 

556 def _convolveExposure(exposure, kernel, convolutionControl, 

557 bbox=None, 

558 psf=None, 

559 photoCalib=None): 

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

561 

562 Parameters 

563 ---------- 

564 exposure : `lsst.afw.Exposure` 

565 exposure to convolve. 

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

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

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

569 Configuration for convolve algorithm. 

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

571 Bounding box to trim the convolved exposure to. 

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

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

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

575 Photometric calibration of the convolved exposure. 

576 

577 Returns 

578 ------- 

579 convolvedExp : `lsst.afw.Exposure` 

580 The convolved image. 

581 """ 

582 convolvedExposure = exposure.clone() 

583 if psf is not None: 

584 convolvedExposure.setPsf(psf) 

585 if photoCalib is not None: 

586 convolvedExposure.setPhotoCalib(photoCalib) 

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

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

589 convolvedExposure.setMaskedImage(convolvedImage) 

590 if bbox is None: 

591 return convolvedExposure 

592 else: 

593 return convolvedExposure[bbox] 

594 

595 def _sourceSelector(self, sources): 

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

597 

598 Parameters 

599 ---------- 

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

601 Input source catalog to select sources from. 

602 

603 Returns 

604 ------- 

605 `lsst.afw.table.SourceCatalog` 

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

607 """ 

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

609 for flag in self.config.badSourceFlags: 

610 try: 

611 flags *= ~sources[flag] 

612 except Exception as e: 

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

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

615 flags *= sToNFlag 

616 selectSources = sources[flags] 

617 

618 return selectSources.copy(deep=True) 

619 

620 

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

622 """Raise NoWorkFound if template coverage < requiredTemplateFraction 

623 

624 Parameters 

625 ---------- 

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

627 The template exposure to check 

628 logger : `lsst.log.Log` 

629 Logger for printing output. 

630 requiredTemplateFraction : `float`, optional 

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

632 in the template. 

633 

634 Raises 

635 ------ 

636 lsst.pipe.base.NoWorkFound 

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

638 set, is less then the configured requiredTemplateFraction 

639 """ 

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

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

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

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

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

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

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

647 

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

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

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

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

652 100*requiredTemplateFraction)) 

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

654 

655 

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

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

658 

659 Parameters 

660 ---------- 

661 science : `lsst.afw.Exposure` 

662 The input science image. 

663 template : `lsst.afw.Exposure` 

664 The template to subtract from the science image. 

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

666 Differential background model 

667 

668 Returns 

669 ------- 

670 difference : `lsst.afw.Exposure` 

671 The subtracted image. 

672 """ 

673 difference = science.clone() 

674 if backgroundModel is not None: 

675 difference.maskedImage -= backgroundModel 

676 difference.maskedImage -= template.maskedImage 

677 return difference 

678 

679 

680def _shapeTest(psf1, psf2): 

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

682 

683 Parameters 

684 ---------- 

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

686 Reference point spread function (PSF) to evaluate. 

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

688 Candidate point spread function (PSF) to evaluate. 

689 

690 Returns 

691 ------- 

692 `bool` 

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

694 """ 

695 shape1 = getPsfFwhm(psf1, average=False) 

696 shape2 = getPsfFwhm(psf2, average=False) 

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

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

699 return xTest | yTest