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

144 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-01 12:22 +0000

1# This file is part of ip_diffim. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

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 

31from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask 

32from lsst.meas.base import SingleFrameMeasurementTask 

33from lsst.pex.exceptions import InvalidParameterError 

34import lsst.pex.config 

35import lsst.pipe.base 

36 

37from .makeKernelBasisList import makeKernelBasisList 

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

39 

40from . import diffimLib 

41from . import diffimTools 

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 : `list` of `dict` 

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

120 field for the Sources deemed to be appropriate for Psf 

121 matching. Can be the output from ``selectKernelSources``. 

122 preconvolved : `bool`, optional 

123 Was the science image convolved with its own PSF? 

124 

125 Returns 

126 ------- 

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

128 

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

130 Spatially varying Psf-matching kernel. 

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

132 Spatially varying background-matching function. 

133 """ 

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

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

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

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

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

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

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

141 try: 

142 templateFwhmPix = getPsfFwhm(template.psf) 

143 scienceFwhmPix = getPsfFwhm(science.psf) 

144 except InvalidParameterError: 

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

146 "Evaluting PSF on a grid of points." 

147 ) 

148 templateFwhmPix = evaluateMeanPsfFwhm(template, 

149 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

150 fwhmExposureGrid=self.config.fwhmExposureGrid 

151 ) 

152 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

153 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

154 fwhmExposureGrid=self.config.fwhmExposureGrid 

155 ) 

156 

157 if preconvolved: 

158 scienceFwhmPix *= np.sqrt(2) 

159 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

160 metadata=self.metadata) 

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

162 return lsst.pipe.base.Struct( 

163 psfMatchingKernel=psfMatchingKernel, 

164 backgroundModel=backgroundModel, 

165 ) 

166 

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

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

169 

170 Parameters 

171 ---------- 

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

173 Exposure that will be convolved. 

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

175 The exposure that will be matched. 

176 candidateList : `list`, optional 

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

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

179 meas.algorithms.PsfCandidateF. 

180 preconvolved : `bool`, optional 

181 Was the science image convolved with its own PSF? 

182 

183 Returns 

184 ------- 

185 kernelSources : `list` of `dict` 

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

187 field for the Sources deemed to be appropriate for Psf 

188 matching. 

189 """ 

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

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

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

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

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

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

196 try: 

197 templateFwhmPix = getPsfFwhm(template.psf) 

198 scienceFwhmPix = getPsfFwhm(science.psf) 

199 except InvalidParameterError: 

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

201 "Evaluting PSF on a grid of points." 

202 ) 

203 templateFwhmPix = evaluateMeanPsfFwhm(template, 

204 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

205 fwhmExposureGrid=self.config.fwhmExposureGrid 

206 ) 

207 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

208 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

209 fwhmExposureGrid=self.config.fwhmExposureGrid 

210 ) 

211 if preconvolved: 

212 scienceFwhmPix *= np.sqrt(2) 

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

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

215 candidateList=candidateList, 

216 preconvolved=preconvolved) 

217 return kernelSources 

218 

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

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

221 

222 This method runs detection and measurement on an exposure. 

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

224 Psf-matching. 

225 

226 Parameters 

227 ---------- 

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

229 Exposure on which to run detection/measurement 

230 sigma : `float`, optional 

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

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

233 doSmooth : `bool` 

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

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

236 Factory for the generation of Source ids 

237 

238 Returns 

239 ------- 

240 selectSources : 

241 source catalog containing candidates for the Psf-matching 

242 """ 

243 if idFactory: 

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

245 else: 

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

247 mi = exposure.getMaskedImage() 

248 

249 imArr = mi.image.array 

250 maskArr = mi.mask.array 

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

252 try: 

253 fitBg = self.background.fitBackground(mi) 

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

255 self.background.config.undersampleStyle) 

256 except Exception: 

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

258 bkgd = np.ma.median(miArr) 

259 

260 # Take off background for detection 

261 mi -= bkgd 

262 try: 

263 table.setMetadata(self.selectAlgMetadata) 

264 detRet = self.selectDetection.run( 

265 table=table, 

266 exposure=exposure, 

267 sigma=sigma, 

268 doSmooth=doSmooth 

269 ) 

270 selectSources = detRet.sources 

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

272 finally: 

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

274 mi += bkgd 

275 del bkgd 

276 return selectSources 

277 

278 def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, 

279 candidateList=None, preconvolved=False): 

280 """Make a list of acceptable KernelCandidates. 

281 

282 Accept or generate a list of candidate sources for 

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

284 images for indications of bad pixels 

285 

286 Parameters 

287 ---------- 

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

289 Exposure that will be convolved 

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

291 Exposure that will be matched-to 

292 kernelSize : `float` 

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

294 candidateList : `list`, optional 

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

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

297 meas.algorithms.PsfCandidateF. 

298 preconvolved : `bool`, optional 

299 Was the science exposure already convolved with its PSF? 

300 

301 Returns 

302 ------- 

303 candidateList : `list` of `dict` 

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

305 field for the Sources deemed to be appropriate for Psf 

306 matching. 

307 

308 Raises 

309 ------ 

310 RuntimeError 

311 If ``candidateList`` is empty or contains incompatible types. 

312 """ 

313 if candidateList is None: 

314 candidateList = self.getSelectSources(scienceExposure, doSmooth=not preconvolved) 

315 

316 if len(candidateList) < 1: 

317 raise RuntimeError("No candidates in candidateList") 

318 

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

320 if len(listTypes) > 1: 

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

322 

323 if not isinstance(candidateList[0], lsst.afw.table.SourceRecord): 

324 try: 

325 candidateList[0].getSource() 

326 except Exception as e: 

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

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

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

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

331 

332 candidateList = diffimTools.sourceToFootprintList(candidateList, 

333 templateExposure, scienceExposure, 

334 kernelSize, 

335 self.kConfig.detectionConfig, 

336 self.log) 

337 if len(candidateList) == 0: 

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

339 

340 return candidateList 

341 

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

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

344 """Wrapper to set log messages for 

345 `lsst.ip.diffim.makeKernelBasisList`. 

346 

347 Parameters 

348 ---------- 

349 targetFwhmPix : `float`, optional 

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

351 Not used for delta function basis sets. 

352 referenceFwhmPix : `float`, optional 

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

354 Not used for delta function basis sets. 

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

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

357 Not used for delta function basis sets. 

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

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

360 Not used for delta function basis sets. 

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

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

363 Not used for delta function basis sets. 

364 

365 Returns 

366 ------- 

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

368 List of basis kernels. 

369 """ 

370 basisList = makeKernelBasisList(self.kConfig, 

371 targetFwhmPix=targetFwhmPix, 

372 referenceFwhmPix=referenceFwhmPix, 

373 basisDegGauss=basisDegGauss, 

374 basisSigmaGauss=basisSigmaGauss, 

375 metadata=metadata) 

376 if targetFwhmPix == referenceFwhmPix: 

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

378 elif referenceFwhmPix > targetFwhmPix: 

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

380 else: 

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

382 

383 return basisList 

384 

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

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

387 

388 Parameters 

389 ---------- 

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

391 MaskedImage to PSF-matched to scienceMaskedImage 

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

393 Reference MaskedImage 

394 candidateList : `list` 

395 A list of footprints/maskedImages for kernel candidates; 

396 

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

398 

399 Returns 

400 ------- 

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

402 a SpatialCellSet for use with self._solve 

403 

404 Raises 

405 ------ 

406 RuntimeError 

407 If no `candidateList` is supplied. 

408 """ 

409 if not candidateList: 

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

411 

412 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

413 

414 imageBBox = templateMaskedImage.getBBox() 

415 imageBBox.clip(scienceMaskedImage.getBBox()) 

416 # Object to store the KernelCandidates for spatial modeling 

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

418 

419 ps = lsst.pex.config.makePropertySet(self.kConfig) 

420 # Place candidates within the spatial grid 

421 for cand in candidateList: 

422 if isinstance(cand, lsst.afw.detection.Footprint): 

423 bbox = cand.getBBox() 

424 else: 

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

426 tmi = lsst.afw.image.MaskedImageF(templateMaskedImage, bbox) 

427 smi = lsst.afw.image.MaskedImageF(scienceMaskedImage, bbox) 

428 

429 if not isinstance(cand, lsst.afw.detection.Footprint): 

430 if 'source' in cand: 

431 cand = cand['source'] 

432 xPos = cand.getCentroid()[0] 

433 yPos = cand.getCentroid()[1] 

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

435 

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

437 kernelCellSet.insertCandidate(cand) 

438 

439 return kernelCellSet 

440 

441 def _adaptCellSize(self, candidateList): 

442 """NOT IMPLEMENTED YET. 

443 

444 Parameters 

445 ---------- 

446 candidateList : `list` 

447 A list of footprints/maskedImages for kernel candidates; 

448 

449 Returns 

450 ------- 

451 sizeCellX, sizeCellY : `int` 

452 New dimensions to use for the kernel. 

453 """ 

454 return self.kConfig.sizeCellX, self.kConfig.sizeCellY