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

130 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-23 02:18 -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 getPsfFwhm 

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 

62 def setDefaults(self): 

63 # High sigma detections only 

64 self.selectDetection.reEstimateBackground = False 

65 self.selectDetection.thresholdValue = 10.0 

66 

67 # Minimal set of measurments for star selection 

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

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

70 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

71 self.selectMeasurement.slots.modelFlux = None 

72 self.selectMeasurement.slots.apFlux = None 

73 self.selectMeasurement.slots.calibFlux = None 

74 

75 

76class MakeKernelTask(PsfMatchTask): 

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

78 """ 

79 

80 ConfigClass = MakeKernelConfig 

81 _DefaultName = "makeALKernel" 

82 

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

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

85 self.kConfig = self.config.kernel.active 

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

87 # so cannot easily be constructed with makeSubtask 

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

89 parentTask=self) 

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

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

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

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

94 

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

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

97 Exposures evaluated at the given source locations. 

98 

99 Parameters 

100 ---------- 

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

102 Exposure that will be convolved. 

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

104 The exposure that will be matched. 

105 kernelSources : `list` of `dict` 

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

107 field for the Sources deemed to be appropriate for Psf 

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

109 preconvolved : `bool`, optional 

110 Was the science image convolved with its own PSF? 

111 

112 Returns 

113 ------- 

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

115 

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

117 Spatially varying Psf-matching kernel. 

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

119 Spatially varying background-matching function. 

120 """ 

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

122 templateFwhmPix = getPsfFwhm(template.psf) 

123 scienceFwhmPix = getPsfFwhm(science.psf) 

124 if preconvolved: 

125 scienceFwhmPix *= np.sqrt(2) 

126 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

127 metadata=self.metadata) 

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

129 return lsst.pipe.base.Struct( 

130 psfMatchingKernel=psfMatchingKernel, 

131 backgroundModel=backgroundModel, 

132 ) 

133 

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

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

136 

137 Parameters 

138 ---------- 

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

140 Exposure that will be convolved. 

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

142 The exposure that will be matched. 

143 candidateList : `list`, optional 

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

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

146 meas.algorithms.PsfCandidateF. 

147 preconvolved : `bool`, optional 

148 Was the science image convolved with its own PSF? 

149 

150 Returns 

151 ------- 

152 kernelSources : `list` of `dict` 

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

154 field for the Sources deemed to be appropriate for Psf 

155 matching. 

156 """ 

157 templateFwhmPix = getPsfFwhm(template.psf) 

158 scienceFwhmPix = getPsfFwhm(science.psf) 

159 if preconvolved: 

160 scienceFwhmPix *= np.sqrt(2) 

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

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

163 candidateList=candidateList, 

164 preconvolved=preconvolved) 

165 return kernelSources 

166 

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

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

169 

170 This method runs detection and measurement on an exposure. 

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

172 Psf-matching. 

173 

174 Parameters 

175 ---------- 

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

177 Exposure on which to run detection/measurement 

178 sigma : `float`, optional 

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

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

181 doSmooth : `bool` 

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

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

184 Factory for the generation of Source ids 

185 

186 Returns 

187 ------- 

188 selectSources : 

189 source catalog containing candidates for the Psf-matching 

190 """ 

191 if idFactory: 

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

193 else: 

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

195 mi = exposure.getMaskedImage() 

196 

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

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

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

200 try: 

201 fitBg = self.background.fitBackground(mi) 

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

203 self.background.config.undersampleStyle) 

204 except Exception: 

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

206 bkgd = np.ma.median(miArr) 

207 

208 # Take off background for detection 

209 mi -= bkgd 

210 try: 

211 table.setMetadata(self.selectAlgMetadata) 

212 detRet = self.selectDetection.run( 

213 table=table, 

214 exposure=exposure, 

215 sigma=sigma, 

216 doSmooth=doSmooth 

217 ) 

218 selectSources = detRet.sources 

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

220 finally: 

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

222 mi += bkgd 

223 del bkgd 

224 return selectSources 

225 

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

227 candidateList=None, preconvolved=False): 

228 """Make a list of acceptable KernelCandidates. 

229 

230 Accept or generate a list of candidate sources for 

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

232 images for indications of bad pixels 

233 

234 Parameters 

235 ---------- 

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

237 Exposure that will be convolved 

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

239 Exposure that will be matched-to 

240 kernelSize : `float` 

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

242 candidateList : `list`, optional 

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

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

245 meas.algorithms.PsfCandidateF. 

246 preconvolved : `bool`, optional 

247 Was the science exposure already convolved with its PSF? 

248 

249 Returns 

250 ------- 

251 candidateList : `list` of `dict` 

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

253 field for the Sources deemed to be appropriate for Psf 

254 matching. 

255 

256 Raises 

257 ------ 

258 RuntimeError 

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

260 """ 

261 if candidateList is None: 

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

263 

264 if len(candidateList) < 1: 

265 raise RuntimeError("No candidates in candidateList") 

266 

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

268 if len(listTypes) > 1: 

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

270 

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

272 try: 

273 candidateList[0].getSource() 

274 except Exception as e: 

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

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

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

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

279 

280 candidateList = diffimTools.sourceToFootprintList(candidateList, 

281 templateExposure, scienceExposure, 

282 kernelSize, 

283 self.kConfig.detectionConfig, 

284 self.log) 

285 if len(candidateList) == 0: 

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

287 

288 return candidateList 

289 

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

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

292 """Wrapper to set log messages for 

293 `lsst.ip.diffim.makeKernelBasisList`. 

294 

295 Parameters 

296 ---------- 

297 targetFwhmPix : `float`, optional 

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

299 Not used for delta function basis sets. 

300 referenceFwhmPix : `float`, optional 

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

302 Not used for delta function basis sets. 

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

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

305 Not used for delta function basis sets. 

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

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

308 Not used for delta function basis sets. 

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

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

311 Not used for delta function basis sets. 

312 

313 Returns 

314 ------- 

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

316 List of basis kernels. 

317 """ 

318 basisList = makeKernelBasisList(self.kConfig, 

319 targetFwhmPix=targetFwhmPix, 

320 referenceFwhmPix=referenceFwhmPix, 

321 basisDegGauss=basisDegGauss, 

322 basisSigmaGauss=basisSigmaGauss, 

323 metadata=metadata) 

324 if targetFwhmPix == referenceFwhmPix: 

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

326 elif referenceFwhmPix > targetFwhmPix: 

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

328 else: 

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

330 

331 return basisList 

332 

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

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

335 

336 Parameters 

337 ---------- 

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

339 MaskedImage to PSF-matched to scienceMaskedImage 

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

341 Reference MaskedImage 

342 candidateList : `list` 

343 A list of footprints/maskedImages for kernel candidates; 

344 

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

346 

347 Returns 

348 ------- 

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

350 a SpatialCellSet for use with self._solve 

351 

352 Raises 

353 ------ 

354 RuntimeError 

355 If no `candidateList` is supplied. 

356 """ 

357 if not candidateList: 

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

359 

360 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

361 

362 imageBBox = templateMaskedImage.getBBox() 

363 imageBBox.clip(scienceMaskedImage.getBBox()) 

364 # Object to store the KernelCandidates for spatial modeling 

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

366 

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

368 # Place candidates within the spatial grid 

369 for cand in candidateList: 

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

371 bbox = cand.getBBox() 

372 else: 

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

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

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

376 

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

378 if 'source' in cand: 

379 cand = cand['source'] 

380 xPos = cand.getCentroid()[0] 

381 yPos = cand.getCentroid()[1] 

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

383 

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

385 kernelCellSet.insertCandidate(cand) 

386 

387 return kernelCellSet 

388 

389 def _adaptCellSize(self, candidateList): 

390 """NOT IMPLEMENTED YET. 

391 

392 Parameters 

393 ---------- 

394 candidateList : `list` 

395 A list of footprints/maskedImages for kernel candidates; 

396 

397 Returns 

398 ------- 

399 sizeCellX, sizeCellY : `int` 

400 New dimensions to use for the kernel. 

401 """ 

402 return self.kConfig.sizeCellX, self.kConfig.sizeCellY