Coverage for python/lsst/ip/diffim/makeKernel.py: 21%

143 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-23 02:08 -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 

22__all__ = ["MakeKernelConfig", "MakeKernelTask"] 

23 

24import numpy as np 

25 

26import lsst.afw.detection 

27import lsst.afw.image 

28import lsst.afw.math 

29import lsst.afw.table 

30import lsst.daf.base 

31import lsst.geom 

32from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask 

33from lsst.meas.base import SingleFrameMeasurementTask 

34from lsst.pex.exceptions import InvalidParameterError 

35import lsst.pex.config 

36import lsst.pipe.base 

37 

38from .makeKernelBasisList import makeKernelBasisList 

39from .psfMatch import PsfMatchConfig, PsfMatchTask, PsfMatchConfigAL, PsfMatchConfigDF 

40 

41from . import diffimLib 

42from .utils import evaluateMeanPsfFwhm, getPsfFwhm 

43 

44 

45class MakeKernelConfig(PsfMatchConfig): 

46 kernel = lsst.pex.config.ConfigChoiceField( 

47 doc="kernel type", 

48 typemap=dict( 

49 AL=PsfMatchConfigAL, 

50 DF=PsfMatchConfigDF 

51 ), 

52 default="AL", 

53 ) 

54 selectDetection = lsst.pex.config.ConfigurableField( 

55 target=SourceDetectionTask, 

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

57 ) 

58 selectMeasurement = lsst.pex.config.ConfigurableField( 

59 target=SingleFrameMeasurementTask, 

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

61 ) 

62 fwhmExposureGrid = lsst.pex.config.Field( 

63 doc="Grid size to compute the average PSF FWHM in an exposure", 

64 dtype=int, 

65 default=10, 

66 ) 

67 fwhmExposureBuffer = lsst.pex.config.Field( 

68 doc="Fractional buffer margin to be left out of all sides of the image during construction" 

69 "of grid to compute average PSF FWHM in an exposure", 

70 dtype=float, 

71 default=0.05, 

72 ) 

73 

74 def setDefaults(self): 

75 # High sigma detections only 

76 self.selectDetection.reEstimateBackground = False 

77 self.selectDetection.thresholdValue = 10.0 

78 self.selectDetection.excludeMaskPlanes = ["EDGE"] 

79 

80 # Minimal set of measurments for star selection 

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

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

83 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

84 self.selectMeasurement.slots.modelFlux = None 

85 self.selectMeasurement.slots.apFlux = None 

86 self.selectMeasurement.slots.calibFlux = None 

87 

88 

89class MakeKernelTask(PsfMatchTask): 

90 """Construct a kernel for PSF matching two exposures. 

91 """ 

92 

93 ConfigClass = MakeKernelConfig 

94 _DefaultName = "makeALKernel" 

95 

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

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

98 self.kConfig = self.config.kernel.active 

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

100 # so cannot easily be constructed with makeSubtask 

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

102 parentTask=self) 

103 self.selectSchema = lsst.afw.table.SourceTable.makeMinimalSchema() 

104 self.selectAlgMetadata = lsst.daf.base.PropertyList() 

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

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

107 

108 def run(self, template, science, kernelSources, preconvolved=False): 

109 """Solve for the kernel and background model that best match two 

110 Exposures evaluated at the given source locations. 

111 

112 Parameters 

113 ---------- 

114 template : `lsst.afw.image.Exposure` 

115 Exposure that will be convolved. 

116 science : `lsst.afw.image.Exposure` 

117 The exposure that will be matched. 

118 kernelSources : `lsst.afw.table.SourceCatalog` 

119 Kernel candidate sources with appropriately sized footprints. 

120 Typically the output of `MakeKernelTask.selectKernelSources`. 

121 preconvolved : `bool`, optional 

122 Was the science image convolved with its own PSF? 

123 

124 Returns 

125 ------- 

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

127 

128 ``psfMatchingKernel`` : `lsst.afw.math.LinearCombinationKernel` 

129 Spatially varying Psf-matching kernel. 

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

131 Spatially varying background-matching function. 

132 """ 

133 kernelCellSet = self._buildCellSet(template.maskedImage, science.maskedImage, kernelSources) 

134 # Calling getPsfFwhm on template.psf fails on some rare occasions when 

135 # the template has no input exposures at the average position of the 

136 # stars. So we try getPsfFwhm first on template, and if that fails we 

137 # evaluate the PSF on a grid specified by fwhmExposure* fields. 

138 # To keep consistent definitions for PSF size on the template and 

139 # science images, we use the same method for both. 

140 try: 

141 templateFwhmPix = getPsfFwhm(template.psf) 

142 scienceFwhmPix = getPsfFwhm(science.psf) 

143 except InvalidParameterError: 

144 self.log.debug("Unable to evaluate PSF at the average position. " 

145 "Evaluting PSF on a grid of points." 

146 ) 

147 templateFwhmPix = evaluateMeanPsfFwhm(template, 

148 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

149 fwhmExposureGrid=self.config.fwhmExposureGrid 

150 ) 

151 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

152 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

153 fwhmExposureGrid=self.config.fwhmExposureGrid 

154 ) 

155 

156 if preconvolved: 

157 scienceFwhmPix *= np.sqrt(2) 

158 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

159 metadata=self.metadata) 

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

161 return lsst.pipe.base.Struct( 

162 psfMatchingKernel=psfMatchingKernel, 

163 backgroundModel=backgroundModel, 

164 ) 

165 

166 def selectKernelSources(self, template, science, candidateList=None, preconvolved=False): 

167 """Select sources from a list of candidates, and extract footprints. 

168 

169 Parameters 

170 ---------- 

171 template : `lsst.afw.image.Exposure` 

172 Exposure that will be convolved. 

173 science : `lsst.afw.image.Exposure` 

174 The exposure that will be matched. 

175 candidateList : `lsst.afw.table.SourceCatalog` 

176 Sources to check as possible kernel candidates. 

177 preconvolved : `bool`, optional 

178 Was the science image convolved with its own PSF? 

179 

180 Returns 

181 ------- 

182 kernelSources : `lsst.afw.table.SourceCatalog` 

183 Kernel candidates with appropriate sized footprints. 

184 """ 

185 # Calling getPsfFwhm on template.psf fails on some rare occasions when 

186 # the template has no input exposures at the average position of the 

187 # stars. So we try getPsfFwhm first on template, and if that fails we 

188 # evaluate the PSF on a grid specified by fwhmExposure* fields. 

189 # To keep consistent definitions for PSF size on the template and 

190 # science images, we use the same method for both. 

191 try: 

192 templateFwhmPix = getPsfFwhm(template.psf) 

193 scienceFwhmPix = getPsfFwhm(science.psf) 

194 except InvalidParameterError: 

195 self.log.debug("Unable to evaluate PSF at the average position. " 

196 "Evaluting PSF on a grid of points." 

197 ) 

198 templateFwhmPix = evaluateMeanPsfFwhm(template, 

199 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

200 fwhmExposureGrid=self.config.fwhmExposureGrid 

201 ) 

202 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

203 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

204 fwhmExposureGrid=self.config.fwhmExposureGrid 

205 ) 

206 if preconvolved: 

207 scienceFwhmPix *= np.sqrt(2) 

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

209 kernelSources = self.makeCandidateList(template, science, kernelSize, 

210 candidateList=candidateList, 

211 preconvolved=preconvolved) 

212 return kernelSources 

213 

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

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

216 

217 This method runs detection and measurement on an exposure. 

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

219 Psf-matching. 

220 

221 Parameters 

222 ---------- 

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

224 Exposure on which to run detection/measurement 

225 sigma : `float`, optional 

226 PSF sigma, in pixels, used for smoothing the image for detection. 

227 If `None`, the PSF width will be used. 

228 doSmooth : `bool` 

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

230 idFactory : `lsst.afw.table.IdFactory` 

231 Factory for the generation of Source ids 

232 

233 Returns 

234 ------- 

235 selectSources : 

236 source catalog containing candidates for the Psf-matching 

237 """ 

238 if idFactory: 

239 table = lsst.afw.table.SourceTable.make(self.selectSchema, idFactory) 

240 else: 

241 table = lsst.afw.table.SourceTable.make(self.selectSchema) 

242 mi = exposure.getMaskedImage() 

243 

244 imArr = mi.image.array 

245 maskArr = mi.mask.array 

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

247 try: 

248 fitBg = self.background.fitBackground(mi) 

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

250 self.background.config.undersampleStyle) 

251 except Exception: 

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

253 bkgd = np.ma.median(miArr) 

254 

255 # Take off background for detection 

256 mi -= bkgd 

257 try: 

258 table.setMetadata(self.selectAlgMetadata) 

259 detRet = self.selectDetection.run( 

260 table=table, 

261 exposure=exposure, 

262 sigma=sigma, 

263 doSmooth=doSmooth 

264 ) 

265 selectSources = detRet.sources 

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

267 finally: 

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

269 mi += bkgd 

270 del bkgd 

271 

272 self.log.info("Selected %d sources via detection measurement.", len(selectSources)) 

273 return selectSources 

274 

275 def makeCandidateList(self, convolved, reference, kernelSize, 

276 candidateList, preconvolved=False): 

277 """Make a list of acceptable KernelCandidates. 

278 

279 Generate a list of candidate sources for Psf-matching, remove sources 

280 with bad pixel masks set or that extend off the image. 

281 

282 Parameters 

283 ---------- 

284 convolved : `lsst.afw.image.Exposure` 

285 Exposure that will be convolved. This is typically the template 

286 image, and may have a large bbox than the reference exposure. 

287 reference : `lsst.afw.image.Exposure` 

288 Exposure that will be matched-to. This is typically the science 

289 image. 

290 kernelSize : `float` 

291 Dimensions of the Psf-matching Kernel, used to set detection 

292 footprints. 

293 candidateList : `lsst.afw.table.SourceCatalog` 

294 List of Sources to examine for kernel candidacy. 

295 preconvolved : `bool`, optional 

296 Was the science exposure already convolved with its PSF? 

297 

298 Returns 

299 ------- 

300 candidates : `lsst.afw.table.SourceCatalog` 

301 Candidates with footprints extended to a ``kernelSize`` box. 

302 

303 Raises 

304 ------ 

305 RuntimeError 

306 If ``candidateList`` is empty after sub-selection. 

307 """ 

308 if candidateList is None: 

309 candidateList = self.getSelectSources(reference, doSmooth=not preconvolved) 

310 if len(candidateList) < 1: 

311 raise RuntimeError("No kernel candidates after detection and measurement.") 

312 

313 bitmask = reference.mask.getPlaneBitMask(self.config.badMaskPlanes) 

314 good = np.ones(len(candidateList), dtype=bool) 

315 # Make all candidates have the same size footprint, based on kernelSize. 

316 for i, candidate in enumerate(candidateList): 

317 # Only use the brightest peak; the input should be pre-deblended! 

318 peak = candidate.getFootprint().getPeaks()[0] 

319 size = 2*kernelSize + 1 # ensure the resulting box is odd 

320 bbox = lsst.geom.Box2I.makeCenteredBox(candidate.getCentroid(), 

321 lsst.geom.Extent2I(size, size)) 

322 boxFootprint = lsst.afw.detection.Footprint(lsst.afw.geom.SpanSet(bbox)) 

323 boxFootprint.addPeak(peak.getFx(), peak.getFy(), peak.getPeakValue()) 

324 candidate.setFootprint(boxFootprint) 

325 

326 # Reject footprints not contained in either image. 

327 if not reference.getBBox().contains(bbox) or not convolved.getBBox().contains(bbox): 

328 good[i] = False 

329 continue 

330 # Reject footprints with any bad mask bits set. 

331 if (reference.subset(bbox).mask.array & bitmask).any(): 

332 good[i] = False 

333 continue 

334 candidates = candidateList[good].copy(deep=True) 

335 

336 self.log.info("Selected %d / %d sources as kernel candidates.", good.sum(), len(candidateList)) 

337 

338 if len(candidates) < 1: 

339 raise RuntimeError("No good kernel candidates available.") 

340 

341 return candidates 

342 

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

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

345 """Wrapper to set log messages for 

346 `lsst.ip.diffim.makeKernelBasisList`. 

347 

348 Parameters 

349 ---------- 

350 targetFwhmPix : `float`, optional 

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

352 Not used for delta function basis sets. 

353 referenceFwhmPix : `float`, optional 

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

355 Not used for delta function basis sets. 

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

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

358 Not used for delta function basis sets. 

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

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

361 Not used for delta function basis sets. 

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

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

364 Not used for delta function basis sets. 

365 

366 Returns 

367 ------- 

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

369 List of basis kernels. 

370 """ 

371 basisList = makeKernelBasisList(self.kConfig, 

372 targetFwhmPix=targetFwhmPix, 

373 referenceFwhmPix=referenceFwhmPix, 

374 basisDegGauss=basisDegGauss, 

375 basisSigmaGauss=basisSigmaGauss, 

376 metadata=metadata) 

377 if targetFwhmPix == referenceFwhmPix: 

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

379 elif referenceFwhmPix > targetFwhmPix: 

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

381 else: 

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

383 

384 return basisList 

385 

386 def _buildCellSet(self, convolved, reference, candidateList): 

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

388 

389 Parameters 

390 ---------- 

391 convolved : `lsst.afw.image.MaskedImage` 

392 MaskedImage to PSF-matched to reference. 

393 reference : `lsst.afw.image.MaskedImage` 

394 Reference MaskedImage. 

395 candidateList : `lsst.afw.table.SourceCatalog` 

396 Kernel candidate sources with footprints. 

397 

398 Returns 

399 ------- 

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

401 A SpatialCellSet for use with self._solve. 

402 """ 

403 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

404 

405 imageBBox = convolved.getBBox() 

406 imageBBox.clip(reference.getBBox()) 

407 # Object to store the KernelCandidates for spatial modeling 

408 kernelCellSet = lsst.afw.math.SpatialCellSet(imageBBox, sizeCellX, sizeCellY) 

409 

410 candidateConfig = lsst.pex.config.makePropertySet(self.kConfig) 

411 # Place candidates within the spatial grid 

412 for candidate in candidateList: 

413 bbox = candidate.getFootprint().getBBox() 

414 templateCutout = lsst.afw.image.MaskedImageF(convolved, bbox) 

415 scienceCutout = lsst.afw.image.MaskedImageF(reference, bbox) 

416 

417 kernelCandidate = diffimLib.makeKernelCandidate(candidate, 

418 templateCutout, 

419 scienceCutout, 

420 candidateConfig) 

421 

422 self.log.debug("Candidate %d at %.2f, %.2f rating=%f", 

423 kernelCandidate.getId(), 

424 kernelCandidate.getXCenter(), 

425 kernelCandidate.getYCenter(), 

426 kernelCandidate.getCandidateRating()) 

427 kernelCellSet.insertCandidate(kernelCandidate) 

428 

429 return kernelCellSet 

430 

431 def _adaptCellSize(self, candidateList): 

432 """NOT IMPLEMENTED YET. 

433 

434 Parameters 

435 ---------- 

436 candidateList : `list` 

437 A list of footprints/maskedImages for kernel candidates; 

438 

439 Returns 

440 ------- 

441 sizeCellX, sizeCellY : `int` 

442 New dimensions to use for the kernel. 

443 """ 

444 return self.kConfig.sizeCellX, self.kConfig.sizeCellY