Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of meas_extensions_gaap 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <http://www.lsstcorp.org/LegalNotices/>. 

22 

23from __future__ import annotations 

24 

25__all__ = ("GaussianizePsfTask", "GaussianizePsfConfig") 

26 

27import numpy as np 

28import scipy.signal 

29 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.geom as geom 

33from lsst.ip.diffim import diffimLib 

34from lsst.ip.diffim.makeKernelBasisList import makeKernelBasisList 

35from lsst.ip.diffim.modelPsfMatch import ModelPsfMatchConfig, ModelPsfMatchTask 

36from lsst.ip.diffim.modelPsfMatch import sigma2fwhm, nextOddInteger 

37import lsst.log as log 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40 

41 

42class GaussianizePsfConfig(ModelPsfMatchConfig): 

43 """Configuration for model-to-model Psf matching.""" 

44 

45 convolutionMethod = pexConfig.ChoiceField( 

46 dtype=str, 

47 doc="Which type of convolution to use", 

48 default="auto", 

49 allowed={ 

50 "direct": "Brute-force real-space convolution", 

51 "fft": "Convolve using FFTs (generally faster)", 

52 "auto": "Choose the faster method between 'direct' and 'fft'", 

53 "overlap-add": "Convolve using the overlap-add method", 

54 } 

55 ) 

56 

57 

58class GaussianizePsfTask(ModelPsfMatchTask): 

59 """Task to make the PSF at a source Gaussian. 

60 

61 This is a specialized version of `lsst.ip.diffim.ModelPsfMatchTask` for 

62 use within the Gaussian-Aperture and PSF (GAaP) photometry plugin. The 

63 `run` method has a different signature and is optimized for multiple calls. 

64 The optimization includes treating PSF as spatially constant within the 

65 footprint of a source and substituting `lsst.afw.math.convolution` method 

66 with scipy.signal's. The PSF is evaluated at the centroid of the source. 

67 Unlike `lsst.ip.diffim.ModelPsfMatchTask`, the assessment of the fit from 

68 residuals is not made. This is assessed via `PsfFlux` in the GAaP plugin. 

69 

70 See also 

71 -------- 

72 ModelPsfMatchTask 

73 """ 

74 ConfigClass = GaussianizePsfConfig 

75 

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

77 super().__init__(*args, **kwargs) 

78 self.kConfig = self.config.kernel.active 

79 self.ps = pexConfig.makePropertySet(self.kConfig) 

80 

81 def run(self, exposure: lsst.afw.image.Exposure, center: lsst.geom.Point2D, # noqa: F821 

82 targetPsfModel: lsst.afw.detection.GaussianPsf, # noqa: F821 

83 kernelSum=1.0, basisSigmaGauss=None) -> pipeBase.Struct: 

84 """Make the PSF of an exposure match a model PSF. 

85 

86 Parameters 

87 ---------- 

88 exposure : `~lsst.afw.image.Exposure` 

89 A (sub-)exposure containing a single (deblended) source being 

90 measured; it must return a valid PSF model via exposure.getPsf(). 

91 center : `~lsst.geom.Point2D` 

92 The centroid position of the source being measured. 

93 targetPsfModel : `~lsst.afw.detection.GaussianPsf` 

94 The model GaussianPsf to which the PSF at the source must be 

95 matched to. 

96 kernelSum : `float`, optional 

97 A multipicative factor to apply to the kernel sum. 

98 basisSigmaGauss: `list` [`float`], optional 

99 The sigma (in pixels) of the Gaussian in the Alard-Lupton basis set 

100 used to express the kernel in. This is used only if ``scaleByFwhm`` 

101 is set to False. If it is not provided, then it defaults to 

102 `config.alardSigGauss`. 

103 

104 Returns 

105 ------- 

106 result : `struct` 

107 - ``psfMatchedExposure`` : the Psf-matched Exposure. 

108 This has the same parent bbox, wcs as ``exposure`` 

109 and ``targetPsfModel`` as its Psf. 

110 - ``psfMatchingKernel`` : Psf-matching kernel. 

111 - ``kernelCellSet`` : SpatialCellSet used to solve for the 

112 Psf-matching kernel. 

113 - ``metadata`` : Metadata generated in making Alard-Lupton basis 

114 set. 

115 """ 

116 maskedImage = exposure.getMaskedImage() 

117 

118 result = self._buildCellSet(exposure, center, targetPsfModel) 

119 kernelCellSet = result.kernelCellSet 

120 targetPsfModel = result.targetPsfModel 

121 fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius()*sigma2fwhm 

122 fwhmModel = targetPsfModel.computeShape().getDeterminantRadius()*sigma2fwhm 

123 

124 basisList = makeKernelBasisList(self.kConfig, fwhmScience, fwhmModel, 

125 basisSigmaGauss=basisSigmaGauss, 

126 metadata=self.metadata) 

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

128 

129 kParameters = np.array(psfMatchingKernel.getKernelParameters()) 

130 kParameters[0] = kernelSum 

131 psfMatchingKernel.setKernelParameters(kParameters) 

132 

133 bbox = exposure.getBBox() 

134 psfMatchedExposure = afwImage.ExposureD(bbox, exposure.getWcs()) 

135 psfMatchedExposure.setPsf(targetPsfModel) 

136 

137 # Normalize the psf-matching kernel while convolving since its 

138 # magnitude is meaningless when PSF-matching one model to another. 

139 kernelImage = afwImage.ImageD(psfMatchingKernel.getDimensions()) 

140 psfMatchingKernel.computeImage(kernelImage, False) 

141 

142 if self.config.convolutionMethod == "overlap-add": 

143 # The order of image arrays is important if mode="same", since the 

144 # returned image array has the same dimensions as the first one. 

145 psfMatchedImageArray = scipy.signal.oaconvolve(maskedImage.image.array, kernelImage.array, 

146 mode="same") 

147 else: 

148 convolutionMethod = self.config.convolutionMethod 

149 if convolutionMethod == "auto": 

150 # Decide if the convolution is faster in real-space or in 

151 # Fourier space? scipy.signal.convolve uses this under the 

152 # hood, but we call here for logging purposes. 

153 convolutionMethod = scipy.signal.choose_conv_method(maskedImage.image.array, 

154 kernelImage.array) 

155 self.log.debug(f"Using {convolutionMethod} method for convolution.") 

156 

157 # The order of image arrays is important if mode="same", since the 

158 # returned array has the same dimensions as the first argument. 

159 psfMatchedImageArray = scipy.signal.convolve(maskedImage.image.array, kernelImage.array, 

160 method=convolutionMethod, mode="same") 

161 

162 psfMatchedImage = afwImage.ImageD(psfMatchedImageArray) 

163 psfMatchedExposure.setImage(psfMatchedImage) 

164 

165 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure, 

166 psfMatchingKernel=psfMatchingKernel, 

167 kernelCellSet=kernelCellSet, 

168 metadata=self.metadata, 

169 ) 

170 

171 def _buildCellSet(self, exposure, center, targetPsfModel) -> pipeBase.Struct: 

172 """Build a SpatialCellSet with one cell for use with the solve method. 

173 

174 This builds a SpatialCellSet containing a single cell and a single 

175 candidate, centered at the location of the source. 

176 

177 Parameters 

178 ---------- 

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

180 The science exposure that will be convolved; must contain a Psf. 

181 center : `lsst.geom.Point2D` 

182 The centroid of the source being measured. 

183 targetPsfModel : `~lsst.afw.detection.GaussianPsf` 

184 Psf model to match to. 

185 

186 Returns 

187 ------- 

188 result : `struct` 

189 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve 

190 - ``targetPsfModel`` : Validated and/or modified 

191 target model used to populate the SpatialCellSet 

192 

193 Notes 

194 ----- 

195 If the target Psf model and science Psf model have different 

196 dimensions, adjust the targetPsfModel (the model to which the 

197 exposure PSF will be matched) to match that of the science Psf. 

198 If the science Psf dimensions vary across the image, 

199 as is common with a WarpedPsf, either pad or clip 

200 (depending on config.padPsf) the dimensions to be constant. 

201 """ 

202 sizeCellX = self.kConfig.sizeCellX 

203 sizeCellY = self.kConfig.sizeCellY 

204 

205 scienceBBox = exposure.getBBox() 

206 scienceBBox.grow(geom.Extent2I(sizeCellX, sizeCellY)) 

207 sciencePsfModel = exposure.getPsf() 

208 dimenR = targetPsfModel.getDimensions() 

209 

210 # Have the size of the region much larger than the bbox, so that 

211 # the ``kernelCellSet`` has only one instance of `SpatialCell`. 

212 regionSize = 10*max(scienceBBox.getWidth(), scienceBBox.getHeight()) 

213 kernelCellSet = afwMath.SpatialCellSet(geom.Box2I(scienceBBox), regionSize) 

214 

215 # Survey the PSF dimensions of the Spatial Cell Set 

216 # to identify the minimum enclosed or maximum bounding square BBox. 

217 scienceMI = self._makePsfMaskedImage(sciencePsfModel, center) 

218 psfWidth, psfHeight = scienceMI.getBBox().getDimensions() 

219 psfSize = max(psfWidth, psfHeight) 

220 

221 if self.config.doAutoPadPsf: 

222 minPsfSize = nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo) 

223 paddingPix = max(0, minPsfSize - psfSize) 

224 else: 

225 if self.config.padPsfBy % 2 != 0: 

226 raise ValueError("Config padPsfBy (%i pixels) must be even number." % 

227 self.config.padPsfBy) 

228 paddingPix = self.config.padPsfBy 

229 

230 if paddingPix > 0: 

231 self.log.info("Padding Science PSF from (%s, %s) to (%s, %s) pixels" % 

232 (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)) 

233 psfSize += paddingPix 

234 

235 # Check that PSF is larger than the matching kernel. 

236 maxKernelSize = psfSize - 1 

237 if maxKernelSize % 2 == 0: 

238 maxKernelSize -= 1 

239 if self.kConfig.kernelSize > maxKernelSize: 

240 message = """ 

241 Kernel size (%d) too big to match Psfs of size %d. 

242 Please reconfigure by setting one of the following: 

243 1) kernel size to <= %d 

244 2) doAutoPadPsf=True 

245 3) padPsfBy to >= %s 

246 """ % (self.kConfig.kernelSize, psfSize, 

247 maxKernelSize, self.kConfig.kernelSize - maxKernelSize) 

248 raise ValueError(message) 

249 

250 dimenS = geom.Extent2I(psfSize, psfSize) 

251 

252 if (dimenR != dimenS): 

253 try: 

254 targetPsfModel = targetPsfModel.resized(psfSize, psfSize) 

255 except Exception as e: 

256 self.log.warn("Zero padding or clipping the target PSF model of type %s and dimensions %s" 

257 " to the science Psf dimensions %s because: %s", 

258 targetPsfModel.__class__.__name__, dimenR, dimenS, e) 

259 dimenR = dimenS 

260 

261 # Make the target kernel image, at location of science subimage. 

262 targetMI = self._makePsfMaskedImage(targetPsfModel, center, dimensions=dimenR) 

263 

264 # Make the kernel image we are going to convolve. 

265 scienceMI = self._makePsfMaskedImage(sciencePsfModel, center, dimensions=dimenR) 

266 

267 # The image to convolve is the science image, to the target Psf. 

268 kc = diffimLib.makeKernelCandidate(center.getX(), center.getY(), scienceMI, targetMI, self.ps) 

269 kernelCellSet.insertCandidate(kc) 

270 

271 return pipeBase.Struct(kernelCellSet=kernelCellSet, 

272 targetPsfModel=targetPsfModel, 

273 ) 

274 

275 def _solve(self, kernelCellSet, basisList): 

276 """Solve for the PSF matching kernel 

277 

278 Parameters 

279 ---------- 

280 kernelCellSet : `~lsst.afw.math.SpatialCellSet` 

281 A SpatialCellSet to use in determining the matching kernel 

282 (typically as provided by _buildCellSet). 

283 basisList : `list` [`~lsst.afw.math.kernel.FixedKernel`] 

284 A sequence of Kernels to be used in the decomposition of the kernel 

285 (typically as provided by makeKernelBasisList). 

286 

287 Returns 

288 ------- 

289 spatialSolution : `~lsst.ip.diffim.KernelSolution` 

290 Solution of convolution kernels. 

291 psfMatchingKernel : `~lsst.afw.math.LinearCombinationKernel` 

292 Spatially varying Psf-matching kernel. 

293 backgroundModel : `~lsst.afw.math.Function2D` 

294 Spatially varying background-matching function. 

295 

296 Raises 

297 ------ 

298 RuntimeError 

299 Raised if unable to determine PSF matching kernel. 

300 """ 

301 # Visitor for the single kernel fit. 

302 if self.useRegularization: 

303 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, self.ps, self.hMat) 

304 else: 

305 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, self.ps) 

306 

307 # Visitor for the kernel sum rejection. 

308 ksv = diffimLib.KernelSumVisitorF(self.ps) 

309 

310 try: 

311 # Make sure there are no uninitialized candidates as 

312 # active occupants of Cell. 

313 kernelCellSet.visitCandidates(singlekv, 1) 

314 

315 # Reject outliers in kernel sum. 

316 ksv.resetKernelSum() 

317 ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE) 

318 kernelCellSet.visitCandidates(ksv, 1) 

319 ksv.processKsumDistribution() 

320 ksv.setMode(diffimLib.KernelSumVisitorF.REJECT) 

321 kernelCellSet.visitCandidates(ksv, 1) 

322 

323 regionBBox = kernelCellSet.getBBox() 

324 spatialkv = diffimLib.BuildSpatialKernelVisitorF(basisList, regionBBox, self.ps) 

325 kernelCellSet.visitCandidates(spatialkv, 1) 

326 spatialkv.solveLinearEquation() 

327 spatialKernel, spatialBackground = spatialkv.getSolutionPair() 

328 spatialSolution = spatialkv.getKernelSolution() 

329 except Exception as e: 

330 self.log.error("ERROR: Unable to calculate psf matching kernel") 

331 log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG, str(e)) 

332 raise e 

333 

334 self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground) 

335 

336 return spatialSolution, spatialKernel, spatialBackground 

337 

338 def _makePsfMaskedImage(self, psfModel, center, dimensions=None) -> afwImage.MaskedImage: 

339 """Make a MaskedImage of a PSF model of specified dimensions. 

340 

341 Parameters 

342 ---------- 

343 psfModel : `~lsst.afw.detection.Psf` 

344 The PSF model whose image is requested. 

345 center : `~lsst.geom.Point2D` 

346 The location at which the PSF image is requested. 

347 dimensions : `~lsst.geom.Box2I`, optional 

348 The bounding box of the PSF image. 

349 

350 Returns 

351 ------- 

352 kernelIm : `~lsst.afw.image.MaskedImage` 

353 Image of the PSF. 

354 """ 

355 rawKernel = psfModel.computeKernelImage(center).convertF() 

356 if dimensions is None: 

357 dimensions = rawKernel.getDimensions() 

358 if rawKernel.getDimensions() == dimensions: 

359 kernelIm = rawKernel 

360 else: 

361 # Make an image of proper size. 

362 kernelIm = afwImage.ImageF(dimensions) 

363 bboxToPlace = geom.Box2I(geom.Point2I((dimensions.getX() - rawKernel.getWidth())//2, 

364 (dimensions.getY() - rawKernel.getHeight())//2), 

365 rawKernel.getDimensions()) 

366 kernelIm.assign(rawKernel, bboxToPlace) 

367 

368 kernelMask = afwImage.Mask(dimensions, 0x0) 

369 kernelVar = afwImage.ImageF(dimensions, 1.0) 

370 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)