Coverage for python/lsst/ip/diffim/imagePsfMatch.py: 13%

277 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-08 02:06 -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.daf.base as dafBase 

25import lsst.pex.config as pexConfig 

26import lsst.afw.detection as afwDetect 

27import lsst.afw.image as afwImage 

28import lsst.afw.math as afwMath 

29import lsst.afw.geom as afwGeom 

30import lsst.afw.table as afwTable 

31import lsst.geom as geom 

32import lsst.pipe.base as pipeBase 

33from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask, WarpedPsf 

34from lsst.meas.base import SingleFrameMeasurementTask 

35from .makeKernelBasisList import makeKernelBasisList 

36from .psfMatch import PsfMatchTask, PsfMatchConfigDF, PsfMatchConfigAL 

37from . import utils as diffimUtils 

38from . import diffimLib 

39from . import diffimTools 

40import lsst.afw.display as afwDisplay 

41from lsst.utils.timer import timeMethod 

42 

43__all__ = ["ImagePsfMatchConfig", "ImagePsfMatchTask", "subtractAlgorithmRegistry"] 

44 

45sigma2fwhm = 2.*np.sqrt(2.*np.log(2.)) 

46 

47 

48class ImagePsfMatchConfig(pexConfig.Config): 

49 """Configuration for image-to-image Psf matching. 

50 """ 

51 kernel = pexConfig.ConfigChoiceField( 

52 doc="kernel type", 

53 typemap=dict( 

54 AL=PsfMatchConfigAL, 

55 DF=PsfMatchConfigDF 

56 ), 

57 default="AL", 

58 ) 

59 selectDetection = pexConfig.ConfigurableField( 

60 target=SourceDetectionTask, 

61 doc="Initial detections used to feed stars to kernel fitting", 

62 ) 

63 selectMeasurement = pexConfig.ConfigurableField( 

64 target=SingleFrameMeasurementTask, 

65 doc="Initial measurements used to feed stars to kernel fitting", 

66 ) 

67 

68 def setDefaults(self): 

69 # High sigma detections only 

70 self.selectDetection.reEstimateBackground = False 

71 self.selectDetection.thresholdValue = 10.0 

72 

73 # Minimal set of measurments for star selection 

74 self.selectMeasurement.algorithms.names.clear() 

75 self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags', 

76 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

77 self.selectMeasurement.slots.modelFlux = None 

78 self.selectMeasurement.slots.apFlux = None 

79 self.selectMeasurement.slots.calibFlux = None 

80 

81 

82class ImagePsfMatchTask(PsfMatchTask): 

83 """Psf-match two MaskedImages or Exposures using the sources in the images. 

84 

85 Parameters 

86 ---------- 

87 args : 

88 Arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 

89 kwargs : 

90 Keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 

91 

92 Notes 

93 ----- 

94 Upon initialization, the kernel configuration is defined by self.config.kernel.active. 

95 The task creates an lsst.afw.math.Warper from the subConfig self.config.kernel.active.warpingConfig. 

96 A schema for the selection and measurement of candidate lsst.ip.diffim.KernelCandidates is 

97 defined, and used to initize subTasks selectDetection (for candidate detection) and selectMeasurement 

98 (for candidate measurement). 

99 

100 Description 

101 

102 Build a Psf-matching kernel using two input images, either as MaskedImages (in which case they need 

103 to be astrometrically aligned) or Exposures (in which case astrometric alignment will happen by 

104 default but may be turned off). This requires a list of input Sources which may be provided 

105 by the calling Task; if not, the Task will perform a coarse source detection 

106 and selection for this purpose. Sources are vetted for signal-to-noise and masked pixels 

107 (in both the template and science image), and substamps around each acceptable 

108 source are extracted and used to create an instance of KernelCandidate. 

109 Each KernelCandidate is then placed within a lsst.afw.math.SpatialCellSet, which is used by an ensemble of 

110 lsst.afw.math.CandidateVisitor instances to build the Psf-matching kernel. These visitors include, in 

111 the order that they are called: BuildSingleKernelVisitor, KernelSumVisitor, BuildSpatialKernelVisitor, 

112 and AssessSpatialKernelVisitor. 

113 

114 Sigma clipping of KernelCandidates is performed as follows: 

115 

116 - BuildSingleKernelVisitor, using the substamp diffim residuals from the per-source kernel fit, 

117 if PsfMatchConfig.singleKernelClipping is True 

118 - KernelSumVisitor, using the mean and standard deviation of the kernel sum from all candidates, 

119 if PsfMatchConfig.kernelSumClipping is True 

120 - AssessSpatialKernelVisitor, using the substamp diffim ressiduals from the spatial kernel fit, 

121 if PsfMatchConfig.spatialKernelClipping is True 

122 

123 The actual solving for the kernel (and differential background model) happens in 

124 lsst.ip.diffim.PsfMatchTask._solve. This involves a loop over the SpatialCellSet that first builds the 

125 per-candidate matching kernel for the requested number of KernelCandidates per cell 

126 (PsfMatchConfig.nStarPerCell). The quality of this initial per-candidate difference image is examined, 

127 using moments of the pixel residuals in the difference image normalized by the square root of the variance 

128 (i.e. sigma); ideally this should follow a normal (0, 1) distribution, 

129 but the rejection thresholds are set 

130 by the config (PsfMatchConfig.candidateResidualMeanMax and PsfMatchConfig.candidateResidualStdMax). 

131 All candidates that pass this initial build are then examined en masse to find the 

132 mean/stdev of the kernel sums across all candidates. 

133 Objects that are significantly above or below the mean, 

134 typically due to variability or sources that are saturated in one image but not the other, 

135 are also rejected.This threshold is defined by PsfMatchConfig.maxKsumSigma. 

136 Finally, a spatial model is built using all currently-acceptable candidates, 

137 and the spatial model used to derive a second set of (spatial) residuals 

138 which are again used to reject bad candidates, using the same thresholds as above. 

139 

140 Invoking the Task 

141 

142 There is no run() method for this Task. Instead there are 4 methods that 

143 may be used to invoke the Psf-matching. These are 

144 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchMaskedImages`, 

145 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractMaskedImages`, 

146 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchExposures`, and 

147 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractExposures`. 

148 

149 The methods that operate on lsst.afw.image.MaskedImage require that the images already be astrometrically 

150 aligned, and are the same shape. The methods that operate on lsst.afw.image.Exposure allow for the 

151 input images to be misregistered and potentially be different sizes; by default a 

152 lsst.afw.math.LanczosWarpingKernel is used to perform the astrometric alignment. The methods 

153 that "match" images return a Psf-matched image, while the methods that "subtract" images 

154 return a Psf-matched and template subtracted image. 

155 

156 See each method's returned lsst.pipe.base.Struct for more details. 

157 

158 Debug variables 

159 

160 The ``pipetask`` command line interface supports a 

161 flag --debug to import @b debug.py from your PYTHONPATH. The relevant contents of debug.py 

162 for this Task include: 

163 

164 .. code-block:: py 

165 

166 import sys 

167 import lsstDebug 

168 def DebugInfo(name): 

169 di = lsstDebug.getInfo(name) 

170 if name == "lsst.ip.diffim.psfMatch": 

171 di.display = True # enable debug output 

172 di.maskTransparency = 80 # display mask transparency 

173 di.displayCandidates = True # show all the candidates and residuals 

174 di.displayKernelBasis = False # show kernel basis functions 

175 di.displayKernelMosaic = True # show kernel realized across the image 

176 di.plotKernelSpatialModel = False # show coefficients of spatial model 

177 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 

178 elif name == "lsst.ip.diffim.imagePsfMatch": 

179 di.display = True # enable debug output 

180 di.maskTransparency = 30 # display mask transparency 

181 di.displayTemplate = True # show full (remapped) template 

182 di.displaySciIm = True # show science image to match to 

183 di.displaySpatialCells = True # show spatial cells 

184 di.displayDiffIm = True # show difference image 

185 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 

186 elif name == "lsst.ip.diffim.diaCatalogSourceSelector": 

187 di.display = False # enable debug output 

188 di.maskTransparency = 30 # display mask transparency 

189 di.displayExposure = True # show exposure with candidates indicated 

190 di.pauseAtEnd = False # pause when done 

191 return di 

192 lsstDebug.Info = DebugInfo 

193 lsstDebug.frame = 1 

194 

195 Note that if you want addional logging info, you may add to your scripts: 

196 

197 .. code-block:: py 

198 

199 import lsst.utils.logging as logUtils 

200 logUtils.trace_set_at("lsst.ip.diffim", 4) 

201 

202 Examples 

203 -------- 

204 A complete example of using ImagePsfMatchTask 

205 

206 Create a subclass of ImagePsfMatchTask that allows us to either match exposures, or subtract exposures: 

207 

208 .. code-block:: none 

209 

210 class MyImagePsfMatchTask(ImagePsfMatchTask): 

211 

212 def __init__(self, args, kwargs): 

213 ImagePsfMatchTask.__init__(self, args, kwargs) 

214 

215 def run(self, templateExp, scienceExp, mode): 

216 if mode == "matchExposures": 

217 return self.matchExposures(templateExp, scienceExp) 

218 elif mode == "subtractExposures": 

219 return self.subtractExposures(templateExp, scienceExp) 

220 

221 And allow the user the freedom to either run the script in default mode, 

222 or point to their own images on disk. 

223 Note that these images must be readable as an lsst.afw.image.Exposure. 

224 

225 We have enabled some minor display debugging in this script via the --debug option. However, if you 

226 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following 

227 block checks for this script: 

228 

229 .. code-block:: py 

230 

231 if args.debug: 

232 try: 

233 import debug 

234 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST 

235 debug.lsstDebug.frame = 3 

236 except ImportError as e: 

237 print(e, file=sys.stderr) 

238 

239 Finally, we call a run method that we define below. 

240 First set up a Config and modify some of the parameters. 

241 E.g. use an "Alard-Lupton" sum-of-Gaussian basis, 

242 fit for a differential background, and use low order spatial 

243 variation in the kernel and background: 

244 

245 .. code-block:: py 

246 

247 def run(args): 

248 # 

249 # Create the Config and use sum of gaussian basis 

250 # 

251 config = ImagePsfMatchTask.ConfigClass() 

252 config.kernel.name = "AL" 

253 config.kernel.active.fitForBackground = True 

254 config.kernel.active.spatialKernelOrder = 1 

255 config.kernel.active.spatialBgOrder = 0 

256 

257 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images 

258 are sent, make some fake data up for the sake of this example script (have a look at the code if you want 

259 more details on generateFakeImages): 

260 

261 .. code-block:: py 

262 

263 # Run the requested method of the Task 

264 if args.template is not None and args.science is not None: 

265 if not os.path.isfile(args.template): 

266 raise FileNotFoundError("Template image %s does not exist" % (args.template)) 

267 if not os.path.isfile(args.science): 

268 raise FileNotFoundError("Science image %s does not exist" % (args.science)) 

269 try: 

270 templateExp = afwImage.ExposureF(args.template) 

271 except Exception as e: 

272 raise RuntimeError("Cannot read template image %s" % (args.template)) 

273 try: 

274 scienceExp = afwImage.ExposureF(args.science) 

275 except Exception as e: 

276 raise RuntimeError("Cannot read science image %s" % (args.science)) 

277 else: 

278 templateExp, scienceExp = generateFakeImages() 

279 config.kernel.active.sizeCellX = 128 

280 config.kernel.active.sizeCellY = 128 

281 

282 Create and run the Task: 

283 

284 .. code-block:: py 

285 

286 # Create the Task 

287 psfMatchTask = MyImagePsfMatchTask(config=config) 

288 # Run the Task 

289 result = psfMatchTask.run(templateExp, scienceExp, args.mode) 

290 

291 And finally provide some optional debugging displays: 

292 

293 .. code-block:: py 

294 

295 if args.debug: 

296 # See if the LSST debug has incremented the frame number; if not start with frame 3 

297 try: 

298 frame = debug.lsstDebug.frame + 1 

299 except Exception: 

300 frame = 3 

301 afwDisplay.Display(frame=frame).mtv(result.matchedExposure, 

302 title="Example script: Matched Template Image") 

303 if "subtractedExposure" in result.getDict(): 

304 afwDisplay.Display(frame=frame + 1).mtv(result.subtractedExposure, 

305 title="Example script: Subtracted Image") 

306 """ 

307 

308 ConfigClass = ImagePsfMatchConfig 

309 

310 def __init__(self, *args, **kwargs): 

311 """Create the ImagePsfMatchTask. 

312 """ 

313 PsfMatchTask.__init__(self, *args, **kwargs) 

314 self.kConfig = self.config.kernel.active 

315 self._warper = afwMath.Warper.fromConfig(self.kConfig.warpingConfig) 

316 # the background subtraction task uses a config from an unusual location, 

317 # so cannot easily be constructed with makeSubtask 

318 self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background", 

319 parentTask=self) 

320 self.selectSchema = afwTable.SourceTable.makeMinimalSchema() 

321 self.selectAlgMetadata = dafBase.PropertyList() 

322 self.makeSubtask("selectDetection", schema=self.selectSchema) 

323 self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata) 

324 

325 @timeMethod 

326 def matchExposures(self, templateExposure, scienceExposure, 

327 templateFwhmPix=None, scienceFwhmPix=None, 

328 candidateList=None, doWarping=True, convolveTemplate=True): 

329 """Warp and PSF-match an exposure to the reference. 

330 

331 Do the following, in order: 

332 

333 - Warp templateExposure to match scienceExposure, 

334 if doWarping True and their WCSs do not already match 

335 - Determine a PSF matching kernel and differential background model 

336 that matches templateExposure to scienceExposure 

337 - Convolve templateExposure by PSF matching kernel 

338 

339 Parameters 

340 ---------- 

341 templateExposure : `lsst.afw.image.Exposure` 

342 Exposure to warp and PSF-match to the reference masked image 

343 scienceExposure : `lsst.afw.image.Exposure` 

344 Exposure whose WCS and PSF are to be matched to 

345 templateFwhmPix :`float` 

346 FWHM (in pixels) of the Psf in the template image (image to convolve) 

347 scienceFwhmPix : `float` 

348 FWHM (in pixels) of the Psf in the science image 

349 candidateList : `list`, optional 

350 a list of footprints/maskedImages for kernel candidates; 

351 if `None` then source detection is run. 

352 

353 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

354 

355 doWarping : `bool` 

356 what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match: 

357 

358 - if `True` then warp ``templateExposure`` to match ``scienceExposure`` 

359 - if `False` then raise an Exception 

360 

361 convolveTemplate : `bool` 

362 Whether to convolve the template image or the science image: 

363 

364 - if `True`, ``templateExposure`` is warped if doWarping, 

365 ``templateExposure`` is convolved 

366 - if `False`, ``templateExposure`` is warped if doWarping, 

367 ``scienceExposure`` is convolved 

368 

369 Returns 

370 ------- 

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

372 An `lsst.pipe.base.Struct` containing these fields: 

373 

374 - ``matchedImage`` : the PSF-matched exposure = 

375 Warped ``templateExposure`` convolved by psfMatchingKernel. This has: 

376 

377 - the same parent bbox, Wcs and PhotoCalib as scienceExposure 

378 - the same filter as templateExposure 

379 - no Psf (because the PSF-matching process does not compute one) 

380 

381 - ``psfMatchingKernel`` : the PSF matching kernel 

382 - ``backgroundModel`` : differential background model 

383 - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel 

384 

385 Raises 

386 ------ 

387 RuntimeError 

388 Raised if doWarping is False and ``templateExposure`` and 

389 ``scienceExposure`` WCSs do not match 

390 """ 

391 if not self._validateWcs(templateExposure, scienceExposure): 

392 if doWarping: 

393 self.log.info("Astrometrically registering template to science image") 

394 templatePsf = templateExposure.getPsf() 

395 # Warp PSF before overwriting exposure 

396 xyTransform = afwGeom.makeWcsPairTransform(templateExposure.getWcs(), 

397 scienceExposure.getWcs()) 

398 psfWarped = WarpedPsf(templatePsf, xyTransform) 

399 templateExposure = self._warper.warpExposure(scienceExposure.getWcs(), 

400 templateExposure, 

401 destBBox=scienceExposure.getBBox()) 

402 templateExposure.setPsf(psfWarped) 

403 else: 

404 self.log.error("ERROR: Input images not registered") 

405 raise RuntimeError("Input images not registered") 

406 

407 if templateFwhmPix is None: 

408 if not templateExposure.hasPsf(): 

409 self.log.warning("No estimate of Psf FWHM for template image") 

410 else: 

411 templateFwhmPix = diffimUtils.getPsfFwhm(templateExposure.psf) 

412 self.log.info("templateFwhmPix: %s", templateFwhmPix) 

413 

414 if scienceFwhmPix is None: 

415 if not scienceExposure.hasPsf(): 

416 self.log.warning("No estimate of Psf FWHM for science image") 

417 else: 

418 scienceFwhmPix = diffimUtils.getPsfFwhm(scienceExposure.psf) 

419 self.log.info("scienceFwhmPix: %s", scienceFwhmPix) 

420 

421 if convolveTemplate: 

422 kernelSize = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix)[0].getWidth() 

423 candidateList = self.makeCandidateList( 

424 templateExposure, scienceExposure, kernelSize, candidateList) 

425 results = self.matchMaskedImages( 

426 templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList, 

427 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) 

428 else: 

429 kernelSize = self.makeKernelBasisList(scienceFwhmPix, templateFwhmPix)[0].getWidth() 

430 candidateList = self.makeCandidateList( 

431 templateExposure, scienceExposure, kernelSize, candidateList) 

432 results = self.matchMaskedImages( 

433 scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList, 

434 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) 

435 

436 psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs()) 

437 psfMatchedExposure.setFilter(templateExposure.getFilter()) 

438 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) 

439 results.warpedExposure = templateExposure 

440 results.matchedExposure = psfMatchedExposure 

441 return results 

442 

443 @timeMethod 

444 def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, 

445 templateFwhmPix=None, scienceFwhmPix=None): 

446 """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage). 

447 

448 Do the following, in order: 

449 

450 - Determine a PSF matching kernel and differential background model 

451 that matches templateMaskedImage to scienceMaskedImage 

452 - Convolve templateMaskedImage by the PSF matching kernel 

453 

454 Parameters 

455 ---------- 

456 templateMaskedImage : `lsst.afw.image.MaskedImage` 

457 masked image to PSF-match to the reference masked image; 

458 must be warped to match the reference masked image 

459 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

460 maskedImage whose PSF is to be matched to 

461 templateFwhmPix : `float` 

462 FWHM (in pixels) of the Psf in the template image (image to convolve) 

463 scienceFwhmPix : `float` 

464 FWHM (in pixels) of the Psf in the science image 

465 candidateList : `list`, optional 

466 A list of footprints/maskedImages for kernel candidates; 

467 if `None` then source detection is run. 

468 

469 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

470 

471 Returns 

472 ------- 

473 result : `callable` 

474 An `lsst.pipe.base.Struct` containing these fields: 

475 

476 - psfMatchedMaskedImage: the PSF-matched masked image = 

477 ``templateMaskedImage`` convolved with psfMatchingKernel. 

478 This has the same xy0, dimensions and wcs as ``scienceMaskedImage``. 

479 - psfMatchingKernel: the PSF matching kernel 

480 - backgroundModel: differential background model 

481 - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel 

482 

483 Raises 

484 ------ 

485 RuntimeError 

486 Raised if input images have different dimensions 

487 """ 

488 import lsstDebug 

489 display = lsstDebug.Info(__name__).display 

490 displayTemplate = lsstDebug.Info(__name__).displayTemplate 

491 displaySciIm = lsstDebug.Info(__name__).displaySciIm 

492 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells 

493 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

494 if not maskTransparency: 

495 maskTransparency = 0 

496 if display: 

497 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

498 

499 if not candidateList: 

500 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

501 

502 if not self._validateSize(templateMaskedImage, scienceMaskedImage): 

503 self.log.error("ERROR: Input images different size") 

504 raise RuntimeError("Input images different size") 

505 

506 if display and displayTemplate: 

507 disp = afwDisplay.Display(frame=lsstDebug.frame) 

508 disp.mtv(templateMaskedImage, title="Image to convolve") 

509 lsstDebug.frame += 1 

510 

511 if display and displaySciIm: 

512 disp = afwDisplay.Display(frame=lsstDebug.frame) 

513 disp.mtv(scienceMaskedImage, title="Image to not convolve") 

514 lsstDebug.frame += 1 

515 

516 kernelCellSet = self._buildCellSet(templateMaskedImage, 

517 scienceMaskedImage, 

518 candidateList) 

519 

520 if display and displaySpatialCells: 

521 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, 

522 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW, 

523 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame, 

524 title="Image to not convolve") 

525 lsstDebug.frame += 1 

526 

527 if templateFwhmPix and scienceFwhmPix: 

528 self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix) 

529 

530 if self.kConfig.useBicForKernelBasis: 

531 tmpKernelCellSet = self._buildCellSet(templateMaskedImage, 

532 scienceMaskedImage, 

533 candidateList) 

534 nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix) 

535 bicDegrees = nbe(tmpKernelCellSet, self.log) 

536 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

537 basisDegGauss=bicDegrees[0], metadata=self.metadata) 

538 del tmpKernelCellSet 

539 else: 

540 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

541 metadata=self.metadata) 

542 

543 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList) 

544 

545 psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox()) 

546 convolutionControl = afwMath.ConvolutionControl() 

547 convolutionControl.setDoNormalize(False) 

548 afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, convolutionControl) 

549 return pipeBase.Struct( 

550 matchedImage=psfMatchedMaskedImage, 

551 psfMatchingKernel=psfMatchingKernel, 

552 backgroundModel=backgroundModel, 

553 kernelCellSet=kernelCellSet, 

554 ) 

555 

556 @timeMethod 

557 def subtractExposures(self, templateExposure, scienceExposure, 

558 templateFwhmPix=None, scienceFwhmPix=None, 

559 candidateList=None, doWarping=True, convolveTemplate=True): 

560 """Register, Psf-match and subtract two Exposures. 

561 

562 Do the following, in order: 

563 

564 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 

565 - Determine a PSF matching kernel and differential background model 

566 that matches templateExposure to scienceExposure 

567 - PSF-match templateExposure to scienceExposure 

568 - Compute subtracted exposure (see return values for equation). 

569 

570 Parameters 

571 ---------- 

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

573 Exposure to PSF-match to scienceExposure 

574 scienceExposure : `lsst.afw.image.ExposureF` 

575 Reference Exposure 

576 templateFwhmPix : `float` 

577 FWHM (in pixels) of the Psf in the template image (image to convolve) 

578 scienceFwhmPix : `float` 

579 FWHM (in pixels) of the Psf in the science image 

580 candidateList : `list`, optional 

581 A list of footprints/maskedImages for kernel candidates; 

582 if `None` then source detection is run. 

583 

584 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

585 

586 doWarping : `bool` 

587 What to do if ``templateExposure``` and ``scienceExposure`` WCSs do 

588 not match: 

589 

590 - if `True` then warp ``templateExposure`` to match ``scienceExposure`` 

591 - if `False` then raise an Exception 

592 

593 convolveTemplate : `bool` 

594 Convolve the template image or the science image 

595 

596 - if `True`, ``templateExposure`` is warped if doWarping, 

597 ``templateExposure`` is convolved 

598 - if `False`, ``templateExposure`` is warped if doWarping, 

599 ``scienceExposure is`` convolved 

600 

601 Returns 

602 ------- 

603 result : `lsst.pipe.base.Struct` 

604 An `lsst.pipe.base.Struct` containing these fields: 

605 

606 - ``subtractedExposure`` : subtracted Exposure 

607 scienceExposure - (matchedImage + backgroundModel) 

608 - ``matchedImage`` : ``templateExposure`` after warping to match 

609 ``templateExposure`` (if doWarping true), 

610 and convolving with psfMatchingKernel 

611 - ``psfMatchingKernel`` : PSF matching kernel 

612 - ``backgroundModel`` : differential background model 

613 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel 

614 """ 

615 results = self.matchExposures( 

616 templateExposure=templateExposure, 

617 scienceExposure=scienceExposure, 

618 templateFwhmPix=templateFwhmPix, 

619 scienceFwhmPix=scienceFwhmPix, 

620 candidateList=candidateList, 

621 doWarping=doWarping, 

622 convolveTemplate=convolveTemplate 

623 ) 

624 # Always inherit WCS and photocalib from science exposure 

625 subtractedExposure = afwImage.ExposureF(scienceExposure, deep=True) 

626 # Note, the decorrelation afterburner re-calculates the variance plane 

627 # from the variance planes of the original exposures. 

628 # That recalculation code must be in accordance with the 

629 # photometric level set here in ``subtractedMaskedImage``. 

630 if convolveTemplate: 

631 subtractedMaskedImage = subtractedExposure.maskedImage 

632 subtractedMaskedImage -= results.matchedExposure.maskedImage 

633 subtractedMaskedImage -= results.backgroundModel 

634 else: 

635 subtractedMaskedImage = subtractedExposure.maskedImage 

636 subtractedMaskedImage[:, :] = results.warpedExposure.maskedImage 

637 subtractedMaskedImage -= results.matchedExposure.maskedImage 

638 subtractedMaskedImage -= results.backgroundModel 

639 

640 # Preserve polarity of differences 

641 subtractedMaskedImage *= -1 

642 

643 # Place back on native photometric scale 

644 subtractedMaskedImage /= results.psfMatchingKernel.computeImage( 

645 afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False) 

646 # We matched to the warped template 

647 subtractedExposure.setPsf(results.warpedExposure.getPsf()) 

648 

649 import lsstDebug 

650 display = lsstDebug.Info(__name__).display 

651 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

652 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

653 if not maskTransparency: 

654 maskTransparency = 0 

655 if display: 

656 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

657 if display and displayDiffIm: 

658 disp = afwDisplay.Display(frame=lsstDebug.frame) 

659 disp.mtv(templateExposure, title="Template") 

660 lsstDebug.frame += 1 

661 disp = afwDisplay.Display(frame=lsstDebug.frame) 

662 disp.mtv(results.matchedExposure, title="Matched template") 

663 lsstDebug.frame += 1 

664 disp = afwDisplay.Display(frame=lsstDebug.frame) 

665 disp.mtv(scienceExposure, title="Science Image") 

666 lsstDebug.frame += 1 

667 disp = afwDisplay.Display(frame=lsstDebug.frame) 

668 disp.mtv(subtractedExposure, title="Difference Image") 

669 lsstDebug.frame += 1 

670 

671 results.subtractedExposure = subtractedExposure 

672 return results 

673 

674 @timeMethod 

675 def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, 

676 templateFwhmPix=None, scienceFwhmPix=None): 

677 """Psf-match and subtract two MaskedImages. 

678 

679 Do the following, in order: 

680 

681 - PSF-match templateMaskedImage to scienceMaskedImage 

682 - Determine the differential background 

683 - Return the difference: scienceMaskedImage 

684 ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel) 

685 

686 Parameters 

687 ---------- 

688 templateMaskedImage : `lsst.afw.image.MaskedImage` 

689 MaskedImage to PSF-match to ``scienceMaskedImage`` 

690 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

691 Reference MaskedImage 

692 templateFwhmPix : `float` 

693 FWHM (in pixels) of the Psf in the template image (image to convolve) 

694 scienceFwhmPix : `float` 

695 FWHM (in pixels) of the Psf in the science image 

696 candidateList : `list`, optional 

697 A list of footprints/maskedImages for kernel candidates; 

698 if `None` then source detection is run. 

699 

700 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

701 

702 Returns 

703 ------- 

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

705 An `lsst.pipe.base.Struct` containing these fields: 

706 

707 - ``subtractedMaskedImage`` : ``scienceMaskedImage`` - (matchedImage + backgroundModel) 

708 - ``matchedImage`` : templateMaskedImage convolved with psfMatchingKernel 

709 - `psfMatchingKernel`` : PSF matching kernel 

710 - ``backgroundModel`` : differential background model 

711 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel 

712 

713 """ 

714 if not candidateList: 

715 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

716 

717 results = self.matchMaskedImages( 

718 templateMaskedImage=templateMaskedImage, 

719 scienceMaskedImage=scienceMaskedImage, 

720 candidateList=candidateList, 

721 templateFwhmPix=templateFwhmPix, 

722 scienceFwhmPix=scienceFwhmPix, 

723 ) 

724 

725 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True) 

726 subtractedMaskedImage -= results.matchedImage 

727 subtractedMaskedImage -= results.backgroundModel 

728 results.subtractedMaskedImage = subtractedMaskedImage 

729 

730 import lsstDebug 

731 display = lsstDebug.Info(__name__).display 

732 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

733 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

734 if not maskTransparency: 

735 maskTransparency = 0 

736 if display: 

737 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

738 if display and displayDiffIm: 

739 disp = afwDisplay.Display(frame=lsstDebug.frame) 

740 disp.mtv(subtractedMaskedImage, title="Subtracted masked image") 

741 lsstDebug.frame += 1 

742 

743 return results 

744 

745 def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None): 

746 """Get sources to use for Psf-matching. 

747 

748 This method runs detection and measurement on an exposure. 

749 The returned set of sources will be used as candidates for 

750 Psf-matching. 

751 

752 Parameters 

753 ---------- 

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

755 Exposure on which to run detection/measurement 

756 sigma : `float` 

757 Detection threshold 

758 doSmooth : `bool` 

759 Whether or not to smooth the Exposure with Psf before detection 

760 idFactory : 

761 Factory for the generation of Source ids 

762 

763 Returns 

764 ------- 

765 selectSources : 

766 source catalog containing candidates for the Psf-matching 

767 """ 

768 if idFactory: 

769 table = afwTable.SourceTable.make(self.selectSchema, idFactory) 

770 else: 

771 table = afwTable.SourceTable.make(self.selectSchema) 

772 mi = exposure.getMaskedImage() 

773 

774 imArr = mi.getImage().getArray() 

775 maskArr = mi.getMask().getArray() 

776 miArr = np.ma.masked_array(imArr, mask=maskArr) 

777 try: 

778 fitBg = self.background.fitBackground(mi) 

779 bkgd = fitBg.getImageF(self.background.config.algorithm, 

780 self.background.config.undersampleStyle) 

781 except Exception: 

782 self.log.warning("Failed to get background model. Falling back to median background estimation") 

783 bkgd = np.ma.median(miArr) 

784 

785 # Take off background for detection 

786 mi -= bkgd 

787 try: 

788 table.setMetadata(self.selectAlgMetadata) 

789 detRet = self.selectDetection.run( 

790 table=table, 

791 exposure=exposure, 

792 sigma=sigma, 

793 doSmooth=doSmooth 

794 ) 

795 selectSources = detRet.sources 

796 self.selectMeasurement.run(measCat=selectSources, exposure=exposure) 

797 finally: 

798 # Put back on the background in case it is needed down stream 

799 mi += bkgd 

800 del bkgd 

801 return selectSources 

802 

803 def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None): 

804 """Make a list of acceptable KernelCandidates. 

805 

806 Accept or generate a list of candidate sources for 

807 Psf-matching, and examine the Mask planes in both of the 

808 images for indications of bad pixels 

809 

810 Parameters 

811 ---------- 

812 templateExposure : `lsst.afw.image.Exposure` 

813 Exposure that will be convolved 

814 scienceExposure : `lsst.afw.image.Exposure` 

815 Exposure that will be matched-to 

816 kernelSize : `float` 

817 Dimensions of the Psf-matching Kernel, used to grow detection footprints 

818 candidateList : `list`, optional 

819 List of Sources to examine. Elements must be of type afw.table.Source 

820 or a type that wraps a Source and has a getSource() method, such as 

821 meas.algorithms.PsfCandidateF. 

822 

823 Returns 

824 ------- 

825 candidateList : `list` of `dict` 

826 A list of dicts having a "source" and "footprint" 

827 field for the Sources deemed to be appropriate for Psf 

828 matching 

829 """ 

830 if candidateList is None: 

831 candidateList = self.getSelectSources(scienceExposure) 

832 

833 if len(candidateList) < 1: 

834 raise RuntimeError("No candidates in candidateList") 

835 

836 listTypes = set(type(x) for x in candidateList) 

837 if len(listTypes) > 1: 

838 raise RuntimeError("Candidate list contains mixed types: %s" % [t for t in listTypes]) 

839 

840 if not isinstance(candidateList[0], afwTable.SourceRecord): 

841 try: 

842 candidateList[0].getSource() 

843 except Exception as e: 

844 raise RuntimeError(f"Candidate List is of type: {type(candidateList[0])} " 

845 "Can only make candidate list from list of afwTable.SourceRecords, " 

846 f"measAlg.PsfCandidateF or other type with a getSource() method: {e}") 

847 candidateList = [c.getSource() for c in candidateList] 

848 

849 candidateList = diffimTools.sourceToFootprintList(candidateList, 

850 templateExposure, scienceExposure, 

851 kernelSize, 

852 self.kConfig.detectionConfig, 

853 self.log) 

854 if len(candidateList) == 0: 

855 raise RuntimeError("Cannot find any objects suitable for KernelCandidacy") 

856 

857 return candidateList 

858 

859 def makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None, 

860 basisDegGauss=None, basisSigmaGauss=None, metadata=None): 

861 """Wrapper to set log messages for 

862 `lsst.ip.diffim.makeKernelBasisList`. 

863 

864 Parameters 

865 ---------- 

866 targetFwhmPix : `float`, optional 

867 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

868 Not used for delta function basis sets. 

869 referenceFwhmPix : `float`, optional 

870 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

871 Not used for delta function basis sets. 

872 basisDegGauss : `list` of `int`, optional 

873 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

874 Not used for delta function basis sets. 

875 basisSigmaGauss : `list` of `int`, optional 

876 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

877 Not used for delta function basis sets. 

878 metadata : `lsst.daf.base.PropertySet`, optional 

879 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

880 Not used for delta function basis sets. 

881 

882 Returns 

883 ------- 

884 basisList: `list` of `lsst.afw.math.kernel.FixedKernel` 

885 List of basis kernels. 

886 """ 

887 basisList = makeKernelBasisList(self.kConfig, 

888 targetFwhmPix=targetFwhmPix, 

889 referenceFwhmPix=referenceFwhmPix, 

890 basisDegGauss=basisDegGauss, 

891 basisSigmaGauss=basisSigmaGauss, 

892 metadata=metadata) 

893 if targetFwhmPix == referenceFwhmPix: 

894 self.log.info("Target and reference psf fwhms are equal, falling back to config values") 

895 elif referenceFwhmPix > targetFwhmPix: 

896 self.log.info("Reference psf fwhm is the greater, normal convolution mode") 

897 else: 

898 self.log.info("Target psf fwhm is the greater, deconvolution mode") 

899 

900 return basisList 

901 

902 def _adaptCellSize(self, candidateList): 

903 """NOT IMPLEMENTED YET. 

904 """ 

905 return self.kConfig.sizeCellX, self.kConfig.sizeCellY 

906 

907 def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList): 

908 """Build a SpatialCellSet for use with the solve method. 

909 

910 Parameters 

911 ---------- 

912 templateMaskedImage : `lsst.afw.image.MaskedImage` 

913 MaskedImage to PSF-matched to scienceMaskedImage 

914 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

915 Reference MaskedImage 

916 candidateList : `list` 

917 A list of footprints/maskedImages for kernel candidates; 

918 

919 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

920 

921 Returns 

922 ------- 

923 kernelCellSet : `lsst.afw.math.SpatialCellSet` 

924 a SpatialCellSet for use with self._solve 

925 """ 

926 if not candidateList: 

927 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

928 

929 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

930 

931 # Object to store the KernelCandidates for spatial modeling 

932 kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(), 

933 sizeCellX, sizeCellY) 

934 

935 ps = pexConfig.makePropertySet(self.kConfig) 

936 # Place candidates within the spatial grid 

937 for cand in candidateList: 

938 if isinstance(cand, afwDetect.Footprint): 

939 bbox = cand.getBBox() 

940 else: 

941 bbox = cand['footprint'].getBBox() 

942 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox) 

943 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox) 

944 

945 if not isinstance(cand, afwDetect.Footprint): 

946 if 'source' in cand: 

947 cand = cand['source'] 

948 xPos = cand.getCentroid()[0] 

949 yPos = cand.getCentroid()[1] 

950 cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, ps) 

951 

952 self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter()) 

953 kernelCellSet.insertCandidate(cand) 

954 

955 return kernelCellSet 

956 

957 def _validateSize(self, templateMaskedImage, scienceMaskedImage): 

958 """Return True if two image-like objects are the same size. 

959 """ 

960 return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions() 

961 

962 def _validateWcs(self, templateExposure, scienceExposure): 

963 """Return True if the WCS of the two Exposures have the same origin and extent. 

964 """ 

965 templateWcs = templateExposure.getWcs() 

966 scienceWcs = scienceExposure.getWcs() 

967 templateBBox = templateExposure.getBBox() 

968 scienceBBox = scienceExposure.getBBox() 

969 

970 # LLC 

971 templateOrigin = templateWcs.pixelToSky(geom.Point2D(templateBBox.getBegin())) 

972 scienceOrigin = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getBegin())) 

973 

974 # URC 

975 templateLimit = templateWcs.pixelToSky(geom.Point2D(templateBBox.getEnd())) 

976 scienceLimit = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getEnd())) 

977 

978 self.log.info("Template Wcs : %f,%f -> %f,%f", 

979 templateOrigin[0], templateOrigin[1], 

980 templateLimit[0], templateLimit[1]) 

981 self.log.info("Science Wcs : %f,%f -> %f,%f", 

982 scienceOrigin[0], scienceOrigin[1], 

983 scienceLimit[0], scienceLimit[1]) 

984 

985 templateBBox = geom.Box2D(templateOrigin.getPosition(geom.degrees), 

986 templateLimit.getPosition(geom.degrees)) 

987 scienceBBox = geom.Box2D(scienceOrigin.getPosition(geom.degrees), 

988 scienceLimit.getPosition(geom.degrees)) 

989 if not (templateBBox.overlaps(scienceBBox)): 

990 raise RuntimeError("Input images do not overlap at all") 

991 

992 if ((templateOrigin != scienceOrigin) 

993 or (templateLimit != scienceLimit) 

994 or (templateExposure.getDimensions() != scienceExposure.getDimensions())): 

995 return False 

996 return True 

997 

998 

999subtractAlgorithmRegistry = pexConfig.makeRegistry( 

1000 doc="A registry of subtraction algorithms for use as a subtask in imageDifference", 

1001) 

1002 

1003subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)