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

132 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-05 02:45 -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 

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 

33import lsst.pex.config 

34import lsst.pipe.base 

35 

36from .makeKernelBasisList import makeKernelBasisList 

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

38 

39from . import diffimLib 

40from . import diffimTools 

41from .utils import evaluateMeanPsfFwhm 

42 

43 

44class MakeKernelConfig(PsfMatchConfig): 

45 kernel = lsst.pex.config.ConfigChoiceField( 

46 doc="kernel type", 

47 typemap=dict( 

48 AL=PsfMatchConfigAL, 

49 DF=PsfMatchConfigDF 

50 ), 

51 default="AL", 

52 ) 

53 selectDetection = lsst.pex.config.ConfigurableField( 

54 target=SourceDetectionTask, 

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

56 ) 

57 selectMeasurement = lsst.pex.config.ConfigurableField( 

58 target=SingleFrameMeasurementTask, 

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

60 ) 

61 fwhmExposureGrid = lsst.pex.config.Field( 

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

63 dtype=int, 

64 default=10, 

65 ) 

66 fwhmExposureBuffer = lsst.pex.config.Field( 

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

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

69 dtype=float, 

70 default=0.05, 

71 ) 

72 

73 def setDefaults(self): 

74 # High sigma detections only 

75 self.selectDetection.reEstimateBackground = False 

76 self.selectDetection.thresholdValue = 10.0 

77 

78 # Minimal set of measurments for star selection 

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

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

81 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

82 self.selectMeasurement.slots.modelFlux = None 

83 self.selectMeasurement.slots.apFlux = None 

84 self.selectMeasurement.slots.calibFlux = None 

85 

86 

87class MakeKernelTask(PsfMatchTask): 

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

89 """ 

90 

91 ConfigClass = MakeKernelConfig 

92 _DefaultName = "makeALKernel" 

93 

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

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

96 self.kConfig = self.config.kernel.active 

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

98 # so cannot easily be constructed with makeSubtask 

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

100 parentTask=self) 

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

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

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

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

105 

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

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

108 Exposures evaluated at the given source locations. 

109 

110 Parameters 

111 ---------- 

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

113 Exposure that will be convolved. 

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

115 The exposure that will be matched. 

116 kernelSources : `list` of `dict` 

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

118 field for the Sources deemed to be appropriate for Psf 

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

120 preconvolved : `bool`, optional 

121 Was the science image convolved with its own PSF? 

122 

123 Returns 

124 ------- 

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

126 

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

128 Spatially varying Psf-matching kernel. 

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

130 Spatially varying background-matching function. 

131 """ 

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

133 templateFwhmPix = evaluateMeanPsfFwhm(template, 

134 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

135 fwhmExposureGrid=self.config.fwhmExposureGrid 

136 ) 

137 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

138 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

139 fwhmExposureGrid=self.config.fwhmExposureGrid 

140 ) 

141 if preconvolved: 

142 scienceFwhmPix *= np.sqrt(2) 

143 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

144 metadata=self.metadata) 

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

146 return lsst.pipe.base.Struct( 

147 psfMatchingKernel=psfMatchingKernel, 

148 backgroundModel=backgroundModel, 

149 ) 

150 

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

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

153 

154 Parameters 

155 ---------- 

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

157 Exposure that will be convolved. 

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

159 The exposure that will be matched. 

160 candidateList : `list`, optional 

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

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

163 meas.algorithms.PsfCandidateF. 

164 preconvolved : `bool`, optional 

165 Was the science image convolved with its own PSF? 

166 

167 Returns 

168 ------- 

169 kernelSources : `list` of `dict` 

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

171 field for the Sources deemed to be appropriate for Psf 

172 matching. 

173 """ 

174 templateFwhmPix = evaluateMeanPsfFwhm(template, 

175 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

176 fwhmExposureGrid=self.config.fwhmExposureGrid 

177 ) 

178 scienceFwhmPix = evaluateMeanPsfFwhm(science, 

179 fwhmExposureBuffer=self.config.fwhmExposureBuffer, 

180 fwhmExposureGrid=self.config.fwhmExposureGrid 

181 ) 

182 if preconvolved: 

183 scienceFwhmPix *= np.sqrt(2) 

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

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

186 candidateList=candidateList, 

187 preconvolved=preconvolved) 

188 return kernelSources 

189 

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

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

192 

193 This method runs detection and measurement on an exposure. 

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

195 Psf-matching. 

196 

197 Parameters 

198 ---------- 

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

200 Exposure on which to run detection/measurement 

201 sigma : `float`, optional 

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

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

204 doSmooth : `bool` 

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

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

207 Factory for the generation of Source ids 

208 

209 Returns 

210 ------- 

211 selectSources : 

212 source catalog containing candidates for the Psf-matching 

213 """ 

214 if idFactory: 

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

216 else: 

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

218 mi = exposure.getMaskedImage() 

219 

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

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

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

223 try: 

224 fitBg = self.background.fitBackground(mi) 

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

226 self.background.config.undersampleStyle) 

227 except Exception: 

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

229 bkgd = np.ma.median(miArr) 

230 

231 # Take off background for detection 

232 mi -= bkgd 

233 try: 

234 table.setMetadata(self.selectAlgMetadata) 

235 detRet = self.selectDetection.run( 

236 table=table, 

237 exposure=exposure, 

238 sigma=sigma, 

239 doSmooth=doSmooth 

240 ) 

241 selectSources = detRet.sources 

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

243 finally: 

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

245 mi += bkgd 

246 del bkgd 

247 return selectSources 

248 

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

250 candidateList=None, preconvolved=False): 

251 """Make a list of acceptable KernelCandidates. 

252 

253 Accept or generate a list of candidate sources for 

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

255 images for indications of bad pixels 

256 

257 Parameters 

258 ---------- 

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

260 Exposure that will be convolved 

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

262 Exposure that will be matched-to 

263 kernelSize : `float` 

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

265 candidateList : `list`, optional 

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

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

268 meas.algorithms.PsfCandidateF. 

269 preconvolved : `bool`, optional 

270 Was the science exposure already convolved with its PSF? 

271 

272 Returns 

273 ------- 

274 candidateList : `list` of `dict` 

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

276 field for the Sources deemed to be appropriate for Psf 

277 matching. 

278 

279 Raises 

280 ------ 

281 RuntimeError 

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

283 """ 

284 if candidateList is None: 

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

286 

287 if len(candidateList) < 1: 

288 raise RuntimeError("No candidates in candidateList") 

289 

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

291 if len(listTypes) > 1: 

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

293 

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

295 try: 

296 candidateList[0].getSource() 

297 except Exception as e: 

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

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

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

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

302 

303 candidateList = diffimTools.sourceToFootprintList(candidateList, 

304 templateExposure, scienceExposure, 

305 kernelSize, 

306 self.kConfig.detectionConfig, 

307 self.log) 

308 if len(candidateList) == 0: 

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

310 

311 return candidateList 

312 

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

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

315 """Wrapper to set log messages for 

316 `lsst.ip.diffim.makeKernelBasisList`. 

317 

318 Parameters 

319 ---------- 

320 targetFwhmPix : `float`, optional 

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

322 Not used for delta function basis sets. 

323 referenceFwhmPix : `float`, optional 

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

325 Not used for delta function basis sets. 

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

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

328 Not used for delta function basis sets. 

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

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

331 Not used for delta function basis sets. 

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

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

334 Not used for delta function basis sets. 

335 

336 Returns 

337 ------- 

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

339 List of basis kernels. 

340 """ 

341 basisList = makeKernelBasisList(self.kConfig, 

342 targetFwhmPix=targetFwhmPix, 

343 referenceFwhmPix=referenceFwhmPix, 

344 basisDegGauss=basisDegGauss, 

345 basisSigmaGauss=basisSigmaGauss, 

346 metadata=metadata) 

347 if targetFwhmPix == referenceFwhmPix: 

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

349 elif referenceFwhmPix > targetFwhmPix: 

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

351 else: 

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

353 

354 return basisList 

355 

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

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

358 

359 Parameters 

360 ---------- 

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

362 MaskedImage to PSF-matched to scienceMaskedImage 

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

364 Reference MaskedImage 

365 candidateList : `list` 

366 A list of footprints/maskedImages for kernel candidates; 

367 

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

369 

370 Returns 

371 ------- 

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

373 a SpatialCellSet for use with self._solve 

374 

375 Raises 

376 ------ 

377 RuntimeError 

378 If no `candidateList` is supplied. 

379 """ 

380 if not candidateList: 

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

382 

383 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

384 

385 imageBBox = templateMaskedImage.getBBox() 

386 imageBBox.clip(scienceMaskedImage.getBBox()) 

387 # Object to store the KernelCandidates for spatial modeling 

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

389 

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

391 # Place candidates within the spatial grid 

392 for cand in candidateList: 

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

394 bbox = cand.getBBox() 

395 else: 

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

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

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

399 

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

401 if 'source' in cand: 

402 cand = cand['source'] 

403 xPos = cand.getCentroid()[0] 

404 yPos = cand.getCentroid()[1] 

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

406 

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

408 kernelCellSet.insertCandidate(cand) 

409 

410 return kernelCellSet 

411 

412 def _adaptCellSize(self, candidateList): 

413 """NOT IMPLEMENTED YET. 

414 

415 Parameters 

416 ---------- 

417 candidateList : `list` 

418 A list of footprints/maskedImages for kernel candidates; 

419 

420 Returns 

421 ------- 

422 sizeCellX, sizeCellY : `int` 

423 New dimensions to use for the kernel. 

424 """ 

425 return self.kConfig.sizeCellX, self.kConfig.sizeCellY