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

143 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-25 04:28 -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 

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 

79 # Minimal set of measurments for star selection 

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

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

82 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

83 self.selectMeasurement.slots.modelFlux = None 

84 self.selectMeasurement.slots.apFlux = None 

85 self.selectMeasurement.slots.calibFlux = None 

86 

87 

88class MakeKernelTask(PsfMatchTask): 

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

90 """ 

91 

92 ConfigClass = MakeKernelConfig 

93 _DefaultName = "makeALKernel" 

94 

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

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

97 self.kConfig = self.config.kernel.active 

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

99 # so cannot easily be constructed with makeSubtask 

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

101 parentTask=self) 

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

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

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

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

106 

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

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

109 Exposures evaluated at the given source locations. 

110 

111 Parameters 

112 ---------- 

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

114 Exposure that will be convolved. 

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

116 The exposure that will be matched. 

117 kernelSources : `list` of `dict` 

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

119 field for the Sources deemed to be appropriate for Psf 

120 matching. Can be the output from ``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 : `list`, optional 

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

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

178 meas.algorithms.PsfCandidateF. 

179 preconvolved : `bool`, optional 

180 Was the science image convolved with its own PSF? 

181 

182 Returns 

183 ------- 

184 kernelSources : `list` of `dict` 

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

186 field for the Sources deemed to be appropriate for Psf 

187 matching. 

188 """ 

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

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

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

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

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

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

195 try: 

196 templateFwhmPix = getPsfFwhm(template.psf) 

197 scienceFwhmPix = getPsfFwhm(science.psf) 

198 except InvalidParameterError: 

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

200 "Evaluting PSF on a grid of points." 

201 ) 

202 templateFwhmPix = evaluateMeanPsfFwhm(template, 

203 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

204 fwhmExposureGrid=self.config.fwhmExposureGrid 

205 ) 

206 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

207 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

208 fwhmExposureGrid=self.config.fwhmExposureGrid 

209 ) 

210 if preconvolved: 

211 scienceFwhmPix *= np.sqrt(2) 

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

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

214 candidateList=candidateList, 

215 preconvolved=preconvolved) 

216 return kernelSources 

217 

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

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

220 

221 This method runs detection and measurement on an exposure. 

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

223 Psf-matching. 

224 

225 Parameters 

226 ---------- 

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

228 Exposure on which to run detection/measurement 

229 sigma : `float`, optional 

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

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

232 doSmooth : `bool` 

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

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

235 Factory for the generation of Source ids 

236 

237 Returns 

238 ------- 

239 selectSources : 

240 source catalog containing candidates for the Psf-matching 

241 """ 

242 if idFactory: 

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

244 else: 

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

246 mi = exposure.getMaskedImage() 

247 

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

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

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

251 try: 

252 fitBg = self.background.fitBackground(mi) 

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

254 self.background.config.undersampleStyle) 

255 except Exception: 

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

257 bkgd = np.ma.median(miArr) 

258 

259 # Take off background for detection 

260 mi -= bkgd 

261 try: 

262 table.setMetadata(self.selectAlgMetadata) 

263 detRet = self.selectDetection.run( 

264 table=table, 

265 exposure=exposure, 

266 sigma=sigma, 

267 doSmooth=doSmooth 

268 ) 

269 selectSources = detRet.sources 

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

271 finally: 

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

273 mi += bkgd 

274 del bkgd 

275 return selectSources 

276 

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

278 candidateList=None, preconvolved=False): 

279 """Make a list of acceptable KernelCandidates. 

280 

281 Accept or generate a list of candidate sources for 

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

283 images for indications of bad pixels 

284 

285 Parameters 

286 ---------- 

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

288 Exposure that will be convolved 

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

290 Exposure that will be matched-to 

291 kernelSize : `float` 

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

293 candidateList : `list`, optional 

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

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

296 meas.algorithms.PsfCandidateF. 

297 preconvolved : `bool`, optional 

298 Was the science exposure already convolved with its PSF? 

299 

300 Returns 

301 ------- 

302 candidateList : `list` of `dict` 

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

304 field for the Sources deemed to be appropriate for Psf 

305 matching. 

306 

307 Raises 

308 ------ 

309 RuntimeError 

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

311 """ 

312 if candidateList is None: 

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

314 

315 if len(candidateList) < 1: 

316 raise RuntimeError("No candidates in candidateList") 

317 

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

319 if len(listTypes) > 1: 

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

321 

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

323 try: 

324 candidateList[0].getSource() 

325 except Exception as e: 

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

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

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

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

330 

331 candidateList = diffimTools.sourceToFootprintList(candidateList, 

332 templateExposure, scienceExposure, 

333 kernelSize, 

334 self.kConfig.detectionConfig, 

335 self.log) 

336 if len(candidateList) == 0: 

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

338 

339 return candidateList 

340 

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

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

343 """Wrapper to set log messages for 

344 `lsst.ip.diffim.makeKernelBasisList`. 

345 

346 Parameters 

347 ---------- 

348 targetFwhmPix : `float`, optional 

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

350 Not used for delta function basis sets. 

351 referenceFwhmPix : `float`, optional 

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

353 Not used for delta function basis sets. 

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

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

356 Not used for delta function basis sets. 

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

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

359 Not used for delta function basis sets. 

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

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

362 Not used for delta function basis sets. 

363 

364 Returns 

365 ------- 

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

367 List of basis kernels. 

368 """ 

369 basisList = makeKernelBasisList(self.kConfig, 

370 targetFwhmPix=targetFwhmPix, 

371 referenceFwhmPix=referenceFwhmPix, 

372 basisDegGauss=basisDegGauss, 

373 basisSigmaGauss=basisSigmaGauss, 

374 metadata=metadata) 

375 if targetFwhmPix == referenceFwhmPix: 

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

377 elif referenceFwhmPix > targetFwhmPix: 

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

379 else: 

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

381 

382 return basisList 

383 

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

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

386 

387 Parameters 

388 ---------- 

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

390 MaskedImage to PSF-matched to scienceMaskedImage 

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

392 Reference MaskedImage 

393 candidateList : `list` 

394 A list of footprints/maskedImages for kernel candidates; 

395 

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

397 

398 Returns 

399 ------- 

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

401 a SpatialCellSet for use with self._solve 

402 

403 Raises 

404 ------ 

405 RuntimeError 

406 If no `candidateList` is supplied. 

407 """ 

408 if not candidateList: 

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

410 

411 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

412 

413 imageBBox = templateMaskedImage.getBBox() 

414 imageBBox.clip(scienceMaskedImage.getBBox()) 

415 # Object to store the KernelCandidates for spatial modeling 

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

417 

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

419 # Place candidates within the spatial grid 

420 for cand in candidateList: 

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

422 bbox = cand.getBBox() 

423 else: 

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

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

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

427 

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

429 if 'source' in cand: 

430 cand = cand['source'] 

431 xPos = cand.getCentroid()[0] 

432 yPos = cand.getCentroid()[1] 

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

434 

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

436 kernelCellSet.insertCandidate(cand) 

437 

438 return kernelCellSet 

439 

440 def _adaptCellSize(self, candidateList): 

441 """NOT IMPLEMENTED YET. 

442 

443 Parameters 

444 ---------- 

445 candidateList : `list` 

446 A list of footprints/maskedImages for kernel candidates; 

447 

448 Returns 

449 ------- 

450 sizeCellX, sizeCellY : `int` 

451 New dimensions to use for the kernel. 

452 """ 

453 return self.kConfig.sizeCellX, self.kConfig.sizeCellY