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

179 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-29 03:01 -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 

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="auto", 

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 

149 forceCompatibility = lsst.pex.config.Field( 

150 dtype=bool, 

151 default=True, 

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

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

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

155 " and will be removed after v24.", 

156 ) 

157 

158 def setDefaults(self): 

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

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

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

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

163 

164 def validate(self): 

165 if self.forceCompatibility: 

166 self.mode = "convolveTemplate" 

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.Chebyshev1Function2D` 

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 lsst.pipe.base.NoWorkFound 

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

266 set, is less then the configured requiredTemplateFraction 

267 """ 

268 self._validateExposures(template, science) 

269 if self.config.doApplyFinalizedPsf: 

270 self._applyExternalCalibrations(science, 

271 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog) 

272 checkTemplateIsSufficient(template, self.log, 

273 requiredTemplateFraction=self.config.requiredTemplateFraction) 

274 if self.config.forceCompatibility: 

275 # Compatibility option to maintain old functionality 

276 # This should be removed in the future! 

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

278 sources = None 

279 sciencePsfSize = getPsfFwhm(science.psf) 

280 templatePsfSize = getPsfFwhm(template.psf) 

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

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

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

284 if sciencePsfSize < templatePsfSize: 

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

286 convolveTemplate = False 

287 else: 

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

289 convolveTemplate = True 

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 

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

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

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

302 # correct ratio. 

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

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

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

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

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

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

309 

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

311 candidateList=sources, 

312 preconvolved=False) 

313 if convolveTemplate: 

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

315 else: 

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

317 

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

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

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

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

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

323 

324 return subtractResults 

325 

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

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

328 from the science image. 

329 

330 Parameters 

331 ---------- 

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

333 Template exposure, warped to match the science exposure. 

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

335 Science exposure to subtract from the template. 

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

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

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

339 images around them. 

340 

341 Returns 

342 ------- 

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

344 

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

346 Result of subtracting template and science. 

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

348 Warped and PSF-matched template exposure. 

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

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

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

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

353 """ 

354 if self.config.forceCompatibility: 

355 # Compatibility option to maintain old behavior 

356 # This should be removed in the future! 

357 template = template[science.getBBox()] 

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

359 

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

361 self.convolutionControl, 

362 bbox=science.getBBox(), 

363 psf=science.psf, 

364 photoCalib=science.getPhotoCalib()) 

365 difference = _subtractImages(science, matchedTemplate, 

366 backgroundModel=(kernelResult.backgroundModel 

367 if self.config.doSubtractBackground else None)) 

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

369 templateMatched=True) 

370 

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

372 matchedTemplate=matchedTemplate, 

373 matchedScience=science, 

374 backgroundModel=kernelResult.backgroundModel, 

375 psfMatchingKernel=kernelResult.psfMatchingKernel) 

376 

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

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

379 

380 Parameters 

381 ---------- 

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

383 Template exposure, warped to match the science exposure. 

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

385 Science exposure to subtract from the template. 

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

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

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

389 images around them. 

390 

391 Returns 

392 ------- 

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

394 

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

396 Result of subtracting template and science. 

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

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

399 is not PSF-matched to the science image. 

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

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

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

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

404 """ 

405 if self.config.forceCompatibility: 

406 # Compatibility option to maintain old behavior 

407 # This should be removed in the future! 

408 template = template[science.getBBox()] 

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

410 modelParams = kernelResult.backgroundModel.getParameters() 

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

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

413 

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

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

416 

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

418 self.convolutionControl, 

419 psf=template.psf) 

420 

421 # Place back on native photometric scale 

422 matchedScience.maskedImage /= norm 

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

424 matchedTemplate.maskedImage /= norm 

425 matchedTemplate.setPhotoCalib(science.getPhotoCalib()) 

426 

427 difference = _subtractImages(matchedScience, matchedTemplate, 

428 backgroundModel=(kernelResult.backgroundModel 

429 if self.config.doSubtractBackground else None)) 

430 

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

432 templateMatched=False) 

433 

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

435 matchedTemplate=matchedTemplate, 

436 matchedScience=matchedScience, 

437 backgroundModel=kernelResult.backgroundModel, 

438 psfMatchingKernel=kernelResult.psfMatchingKernel,) 

439 

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

441 templateMatched=True, 

442 preConvMode=False, 

443 preConvKernel=None, 

444 spatiallyVarying=False): 

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

446 caused by convolution. 

447 

448 Parameters 

449 ---------- 

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

451 Template exposure, warped to match the science exposure. 

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

453 Science exposure to subtract from the template. 

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

455 Result of subtracting template and science. 

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

457 An (optionally spatially-varying) PSF matching kernel 

458 templateMatched : `bool`, optional 

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

460 preConvMode : `bool`, optional 

461 Was the science image preconvolved with its own PSF 

462 before PSF matching the template? 

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

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

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

466 spatiallyVarying : `bool`, optional 

467 Compute the decorrelation kernel spatially varying across the image? 

468 

469 Returns 

470 ------- 

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

472 The decorrelated image difference. 

473 """ 

474 # Erase existing detection mask planes. 

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

476 mask = difference.mask 

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

478 

479 if self.config.doDecorrelation: 

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

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

482 templateMatched=templateMatched, 

483 preConvMode=preConvMode, 

484 preConvKernel=preConvKernel, 

485 spatiallyVarying=spatiallyVarying).correctedExposure 

486 else: 

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

488 correctedExposure = difference 

489 return correctedExposure 

490 

491 @staticmethod 

492 def _validateExposures(template, science): 

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

494 contains the science bbox. 

495 

496 Parameters 

497 ---------- 

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

499 Template exposure, warped to match the science exposure. 

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

501 Science exposure to subtract from the template. 

502 

503 Raises 

504 ------ 

505 AssertionError 

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

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

508 bounding box. 

509 """ 

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

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

512 templateBBox = template.getBBox() 

513 scienceBBox = science.getBBox() 

514 

515 assert templateBBox.contains(scienceBBox),\ 

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

517 

518 @staticmethod 

519 def _convolveExposure(exposure, kernel, convolutionControl, 

520 bbox=None, 

521 psf=None, 

522 photoCalib=None): 

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

524 

525 Parameters 

526 ---------- 

527 exposure : `lsst.afw.Exposure` 

528 exposure to convolve. 

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

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

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

532 Configuration for convolve algorithm. 

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

534 Bounding box to trim the convolved exposure to. 

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

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

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

538 Photometric calibration of the convolved exposure. 

539 

540 Returns 

541 ------- 

542 convolvedExp : `lsst.afw.Exposure` 

543 The convolved image. 

544 """ 

545 convolvedExposure = exposure.clone() 

546 if psf is not None: 

547 convolvedExposure.setPsf(psf) 

548 if photoCalib is not None: 

549 convolvedExposure.setPhotoCalib(photoCalib) 

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

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

552 convolvedExposure.setMaskedImage(convolvedImage) 

553 if bbox is None: 

554 return convolvedExposure 

555 else: 

556 return convolvedExposure[bbox] 

557 

558 

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

560 """Raise NoWorkFound if template coverage < requiredTemplateFraction 

561 

562 Parameters 

563 ---------- 

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

565 The template exposure to check 

566 logger : `lsst.log.Log` 

567 Logger for printing output. 

568 requiredTemplateFraction : `float`, optional 

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

570 in the template. 

571 

572 Raises 

573 ------ 

574 lsst.pipe.base.NoWorkFound 

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

576 set, is less then the configured requiredTemplateFraction 

577 """ 

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

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

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

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

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

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

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

585 

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

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

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

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

590 100*requiredTemplateFraction)) 

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

592 

593 

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

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

596 

597 Parameters 

598 ---------- 

599 science : `lsst.afw.Exposure` 

600 The input science image. 

601 template : `lsst.afw.Exposure` 

602 The template to subtract from the science image. 

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

604 Differential background model 

605 

606 Returns 

607 ------- 

608 difference : `lsst.afw.Exposure` 

609 The subtracted image. 

610 """ 

611 difference = science.clone() 

612 if backgroundModel is not None: 

613 difference.maskedImage -= backgroundModel 

614 difference.maskedImage -= template.maskedImage 

615 return difference