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

177 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-27 02:32 -0700

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 

33 

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

35 

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

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

38 

39 

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

41 dimensions=_dimensions, 

42 defaultTemplates=_defaultTemplates): 

43 template = connectionTypes.Input( 

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

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

46 storageClass="ExposureF", 

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

48 ) 

49 science = connectionTypes.Input( 

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

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

52 storageClass="ExposureF", 

53 name="{fakesType}calexp" 

54 ) 

55 sources = connectionTypes.Input( 

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

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

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

59 storageClass="SourceCatalog", 

60 name="{fakesType}src" 

61 ) 

62 finalizedPsfApCorrCatalog = connectionTypes.Input( 

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

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

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

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

67 storageClass="ExposureCatalog", 

68 name="finalized_psf_ap_corr_catalog", 

69 ) 

70 

71 

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

73 dimensions=_dimensions, 

74 defaultTemplates=_defaultTemplates): 

75 difference = connectionTypes.Output( 

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

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

78 storageClass="ExposureF", 

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

80 ) 

81 matchedTemplate = connectionTypes.Output( 

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

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

84 storageClass="ExposureF", 

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

86 ) 

87 

88 

89class AlardLuptonSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections): 

90 

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

92 super().__init__(config=config) 

93 if not config.doApplyFinalizedPsf: 

94 self.inputs.remove("finalizedPsfApCorrCatalog") 

95 

96 

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

98 pipelineConnections=AlardLuptonSubtractConnections): 

99 mode = lsst.pex.config.ChoiceField( 

100 dtype=str, 

101 default="auto", 

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

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

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

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

106 ) 

107 makeKernel = lsst.pex.config.ConfigurableField( 

108 target=MakeKernelTask, 

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

110 ) 

111 doDecorrelation = lsst.pex.config.Field( 

112 dtype=bool, 

113 default=True, 

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

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

116 ) 

117 decorrelate = lsst.pex.config.ConfigurableField( 

118 target=DecorrelateALKernelTask, 

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

120 ) 

121 requiredTemplateFraction = lsst.pex.config.Field( 

122 dtype=float, 

123 default=0.1, 

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

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

126 ) 

127 doScaleVariance = lsst.pex.config.Field( 

128 dtype=bool, 

129 default=True, 

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

131 ) 

132 scaleVariance = lsst.pex.config.ConfigurableField( 

133 target=ScaleVarianceTask, 

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

135 ) 

136 doSubtractBackground = lsst.pex.config.Field( 

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

138 dtype=bool, 

139 default=True, 

140 ) 

141 doApplyFinalizedPsf = lsst.pex.config.Field( 

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

143 " with those in finalizedPsfApCorrCatalog.", 

144 dtype=bool, 

145 default=False, 

146 ) 

147 

148 forceCompatibility = lsst.pex.config.Field( 

149 dtype=bool, 

150 default=False, 

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

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

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

154 " and will be removed after v24.", 

155 ) 

156 

157 def setDefaults(self): 

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

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

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

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

162 

163 def validate(self): 

164 if self.forceCompatibility: 

165 self.mode = "convolveTemplate" 

166 

167 

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

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

170 the Alard & Lupton (1998) algorithm. 

171 """ 

172 ConfigClass = AlardLuptonSubtractConfig 

173 _DefaultName = "alardLuptonSubtract" 

174 

175 def __init__(self, **kwargs): 

176 super().__init__(**kwargs) 

177 self.makeSubtask("decorrelate") 

178 self.makeSubtask("makeKernel") 

179 if self.config.doScaleVariance: 

180 self.makeSubtask("scaleVariance") 

181 

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

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

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

185 self.convolutionControl.setDoNormalize(False) 

186 self.convolutionControl.setDoCopyEdge(True) 

187 

188 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog): 

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

190 

191 Parameters 

192 ---------- 

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

194 Input exposure to adjust calibrations. 

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

196 Exposure catalog with finalized psf models and aperture correction 

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

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

199 

200 Returns 

201 ------- 

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

203 Exposure with adjusted calibrations. 

204 """ 

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

206 

207 row = finalizedPsfApCorrCatalog.find(detectorId) 

208 if row is None: 

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

210 "Using original psf.", detectorId) 

211 else: 

212 psf = row.getPsf() 

213 apCorrMap = row.getApCorrMap() 

214 if psf is None: 

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

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

217 detectorId) 

218 elif apCorrMap is None: 

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

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

221 detectorId) 

222 else: 

223 exposure.setPsf(psf) 

224 exposure.info.setApCorrMap(apCorrMap) 

225 

226 return exposure 

227 

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

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

230 

231 Parameters 

232 ---------- 

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

234 Template exposure, warped to match the science exposure. 

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

236 Science exposure to subtract from the template. 

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

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

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

240 images around them. 

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

242 Exposure catalog with finalized psf models and aperture correction 

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

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

245 

246 Returns 

247 ------- 

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

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

250 Result of subtracting template and science. 

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

252 Warped and PSF-matched template exposure. 

253 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D` 

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

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

256 Kernel used to PSF-match the convolved image. 

257 

258 Raises 

259 ------ 

260 RuntimeError 

261 If an unsupported convolution mode is supplied. 

262 lsst.pipe.base.NoWorkFound 

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

264 set, is less then the configured requiredTemplateFraction 

265 """ 

266 self._validateExposures(template, science) 

267 if self.config.doApplyFinalizedPsf: 

268 self._applyExternalCalibrations(science, 

269 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog) 

270 checkTemplateIsSufficient(template, self.log, 

271 requiredTemplateFraction=self.config.requiredTemplateFraction) 

272 if self.config.forceCompatibility: 

273 # Compatibility option to maintain old functionality 

274 # This should be removed in the future! 

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

276 sources = None 

277 sciencePsfSize = getPsfFwhm(science.psf) 

278 templatePsfSize = getPsfFwhm(template.psf) 

279 self.log.info("Science PSF size: %f", sciencePsfSize) 

280 self.log.info("Template PSF size: %f", templatePsfSize) 

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

282 if sciencePsfSize < templatePsfSize: 

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

284 convolveTemplate = False 

285 else: 

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

287 convolveTemplate = True 

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

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

290 convolveTemplate = True 

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

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

293 convolveTemplate = False 

294 else: 

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

296 

297 if self.config.doScaleVariance and ~self.config.forceCompatibility: 

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

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

300 # correct ratio. 

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

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

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

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

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

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

307 

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

309 candidateList=sources, 

310 preconvolved=False) 

311 if convolveTemplate: 

312 subtractResults = self.runConvolveTemplate(template, science, kernelSources) 

313 else: 

314 subtractResults = self.runConvolveScience(template, science, kernelSources) 

315 

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

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

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

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

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

321 

322 return subtractResults 

323 

324 def runConvolveTemplate(self, template, science, sources): 

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

326 from the science image. 

327 

328 Parameters 

329 ---------- 

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

331 Template exposure, warped to match the science exposure. 

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

333 Science exposure to subtract from the template. 

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

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

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

337 images around them. 

338 

339 Returns 

340 ------- 

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

342 

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

344 Result of subtracting template and science. 

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

346 Warped and PSF-matched template exposure. 

347 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D` 

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

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

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

351 """ 

352 if self.config.forceCompatibility: 

353 # Compatibility option to maintain old behavior 

354 # This should be removed in the future! 

355 template = template[science.getBBox()] 

356 kernelResult = self.makeKernel.run(template, science, sources, preconvolved=False) 

357 

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

359 self.convolutionControl, 

360 bbox=science.getBBox(), 

361 psf=science.psf, 

362 photoCalib=science.getPhotoCalib()) 

363 difference = _subtractImages(science, matchedTemplate, 

364 backgroundModel=(kernelResult.backgroundModel 

365 if self.config.doSubtractBackground else None)) 

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

367 templateMatched=True) 

368 

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

370 matchedTemplate=matchedTemplate, 

371 matchedScience=science, 

372 backgroundModel=kernelResult.backgroundModel, 

373 psfMatchingKernel=kernelResult.psfMatchingKernel) 

374 

375 def runConvolveScience(self, template, science, sources): 

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

377 

378 Parameters 

379 ---------- 

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

381 Template exposure, warped to match the science exposure. 

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

383 Science exposure to subtract from the template. 

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

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

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

387 images around them. 

388 

389 Returns 

390 ------- 

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

392 

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

394 Result of subtracting template and science. 

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

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

397 is not PSF-matched to the science image. 

398 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D` 

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

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

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

402 """ 

403 if self.config.forceCompatibility: 

404 # Compatibility option to maintain old behavior 

405 # This should be removed in the future! 

406 template = template[science.getBBox()] 

407 kernelResult = self.makeKernel.run(science, template, sources, preconvolved=False) 

408 modelParams = kernelResult.backgroundModel.getParameters() 

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

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

411 

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

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

414 

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

416 self.convolutionControl, 

417 psf=template.psf) 

418 

419 # Place back on native photometric scale 

420 matchedScience.maskedImage /= norm 

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

422 matchedTemplate.maskedImage /= norm 

423 matchedTemplate.setPhotoCalib(science.getPhotoCalib()) 

424 

425 difference = _subtractImages(matchedScience, matchedTemplate, 

426 backgroundModel=(kernelResult.backgroundModel 

427 if self.config.doSubtractBackground else None)) 

428 

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

430 templateMatched=False) 

431 

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

433 matchedTemplate=matchedTemplate, 

434 matchedScience=matchedScience, 

435 backgroundModel=kernelResult.backgroundModel, 

436 psfMatchingKernel=kernelResult.psfMatchingKernel,) 

437 

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

439 templateMatched=True, 

440 preConvMode=False, 

441 preConvKernel=None, 

442 spatiallyVarying=False): 

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

444 caused by convolution. 

445 

446 Parameters 

447 ---------- 

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

449 Template exposure, warped to match the science exposure. 

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

451 Science exposure to subtract from the template. 

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

453 Result of subtracting template and science. 

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

455 An (optionally spatially-varying) PSF matching kernel 

456 templateMatched : `bool`, optional 

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

458 preConvMode : `bool`, optional 

459 Was the science image preconvolved with its own PSF 

460 before PSF matching the template? 

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

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

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

464 spatiallyVarying : `bool`, optional 

465 Compute the decorrelation kernel spatially varying across the image? 

466 

467 Returns 

468 ------- 

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

470 The decorrelated image difference. 

471 """ 

472 # Erase existing detection mask planes. 

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

474 mask = difference.mask 

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

476 

477 if self.config.doDecorrelation: 

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

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

480 templateMatched=templateMatched, 

481 preConvMode=preConvMode, 

482 preConvKernel=preConvKernel, 

483 spatiallyVarying=spatiallyVarying).correctedExposure 

484 else: 

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

486 correctedExposure = difference 

487 return correctedExposure 

488 

489 @staticmethod 

490 def _validateExposures(template, science): 

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

492 contains the science bbox. 

493 

494 Parameters 

495 ---------- 

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

497 Template exposure, warped to match the science exposure. 

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

499 Science exposure to subtract from the template. 

500 

501 Raises 

502 ------ 

503 AssertionError 

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

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

506 bounding box. 

507 """ 

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

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

510 templateBBox = template.getBBox() 

511 scienceBBox = science.getBBox() 

512 

513 assert templateBBox.contains(scienceBBox),\ 

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

515 

516 @staticmethod 

517 def _convolveExposure(exposure, kernel, convolutionControl, 

518 bbox=None, 

519 psf=None, 

520 photoCalib=None): 

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

522 

523 Parameters 

524 ---------- 

525 exposure : `lsst.afw.Exposure` 

526 exposure to convolve. 

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

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

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

530 Configuration for convolve algorithm. 

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

532 Bounding box to trim the convolved exposure to. 

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

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

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

536 Photometric calibration of the convolved exposure. 

537 

538 Returns 

539 ------- 

540 convolvedExp : `lsst.afw.Exposure` 

541 The convolved image. 

542 """ 

543 convolvedExposure = exposure.clone() 

544 if psf is not None: 

545 convolvedExposure.setPsf(psf) 

546 if photoCalib is not None: 

547 convolvedExposure.setPhotoCalib(photoCalib) 

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

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

550 convolvedExposure.setMaskedImage(convolvedImage) 

551 if bbox is None: 

552 return convolvedExposure 

553 else: 

554 return convolvedExposure[bbox] 

555 

556 

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

558 """Raise NoWorkFound if template coverage < requiredTemplateFraction 

559 

560 Parameters 

561 ---------- 

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

563 The template exposure to check 

564 logger : `lsst.log.Log` 

565 Logger for printing output. 

566 requiredTemplateFraction : `float`, optional 

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

568 in the template. 

569 

570 Raises 

571 ------ 

572 lsst.pipe.base.NoWorkFound 

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

574 set, is less then the configured requiredTemplateFraction 

575 """ 

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

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

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

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

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

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

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

583 

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

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

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

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

588 100*requiredTemplateFraction)) 

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

590 

591 

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

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

594 

595 Parameters 

596 ---------- 

597 science : `lsst.afw.Exposure` 

598 The input science image. 

599 template : `lsst.afw.Exposure` 

600 The template to subtract from the science image. 

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

602 Differential background model 

603 

604 Returns 

605 ------- 

606 difference : `lsst.afw.Exposure` 

607 The subtracted image. 

608 """ 

609 difference = science.clone() 

610 if backgroundModel is not None: 

611 difference.maskedImage -= backgroundModel 

612 difference.maskedImage -= template.maskedImage 

613 return difference