LSST Applications 24.1.6,g063fba187b+e7121a6b04,g0f08755f38+4e0faf0f7f,g12f32b3c4e+7915c4de30,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g28da252d5a+94d9f37a33,g2bbee38e9b+ae03bbfc84,g2bc492864f+ae03bbfc84,g3156d2b45e+6e55a43351,g347aa1857d+ae03bbfc84,g35bb328faa+a8ce1bb630,g3a166c0a6a+ae03bbfc84,g3e281a1b8c+c5dd892a6c,g414038480c+6b9177ef31,g41af890bb2+9e154f3e8d,g6b1c1869cb+adc49b6f1a,g781aacb6e4+a8ce1bb630,g7af13505b9+3363a39af3,g7f202ee025+406ba613a5,g80478fca09+8fbba356e2,g82479be7b0+0d223595df,g858d7b2824+4e0faf0f7f,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,g9726552aa6+414189b318,ga5288a1d22+32d6120315,gacef1a1666+7f85da65db,gb58c049af0+d64f4d3760,gbcfae0f0a0+a8c62e8bb6,gc28159a63d+ae03bbfc84,gcf0d15dbbd+412a8a6f35,gda6a2b7d83+412a8a6f35,gdaeeff99f8+1711a396fd,ge79ae78c31+ae03bbfc84,gf0baf85859+c1f95f4921,gfa517265be+4e0faf0f7f,gfa999e8aa5+17cd334064,gfb92a5be7c+4e0faf0f7f
LSST Data Management Base Package
Loading...
Searching...
No Matches
modelPsfMatch.py
Go to the documentation of this file.
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
22import numpy as np
23
24from . import diffimLib
25import lsst.afw.display as afwDisplay
26import lsst.afw.image as afwImage
27import lsst.afw.math as afwMath
28import lsst.geom as geom
29import lsst.pex.config as pexConfig
30import lsst.pipe.base as pipeBase
31from lsst.utils.logging import getTraceLogger
32from lsst.utils.timer import timeMethod
33from .makeKernelBasisList import makeKernelBasisList
34from .psfMatch import PsfMatchTask, PsfMatchConfigAL
35from . import utils as dituils
36
37__all__ = ("ModelPsfMatchTask", "ModelPsfMatchConfig")
38
39sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
40
41
43 nextInt = int(np.ceil(x))
44 return nextInt + 1 if nextInt%2 == 0 else nextInt
45
46
47class ModelPsfMatchConfig(pexConfig.Config):
48 """Configuration for model-to-model Psf matching"""
49
50 kernel = pexConfig.ConfigChoiceField(
51 doc="kernel type",
52 typemap=dict(
53 AL=PsfMatchConfigAL,
54 ),
55 default="AL",
56 )
57 doAutoPadPsf = pexConfig.Field(
58 dtype=bool,
59 doc=("If too small, automatically pad the science Psf? "
60 "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
61 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
62 default=True,
63 )
64 autoPadPsfTo = pexConfig.RangeField(
65 dtype=float,
66 doc=("Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
67 "If the dimensions of the Psf to be matched are less than the "
68 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
69 "Ignored if doAutoPadPsf=False."),
70 default=1.4,
71 min=1.0,
72 max=2.0
73 )
74 padPsfBy = pexConfig.Field(
75 dtype=int,
76 doc="Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
77 default=0,
78 )
79
80 def setDefaults(self):
81 # No sigma clipping
82 self.kernel.active.singleKernelClipping = False
83 self.kernel.active.kernelSumClipping = False
84 self.kernel.active.spatialKernelClipping = False
85 self.kernel.active.checkConditionNumber = False
86
87 # Variance is ill defined
88 self.kernel.active.constantVarianceWeighting = True
89
90 # Do not change specified kernel size
91 self.kernel.active.scaleByFwhm = False
92
93
95 """Match two model Psfs, and application of the Psf-matching kernel
96 to an input Exposure.
97 """
98 ConfigClass = ModelPsfMatchConfig
99
100 def __init__(self, *args, **kwargs):
101 PsfMatchTask.__init__(self, *args, **kwargs)
102 self.kConfigkConfig = self.config.kernel.active
103
104 @timeMethod
105 def run(self, exposure, referencePsfModel, kernelSum=1.0):
106 """Psf-match an exposure to a model Psf.
107
108 Parameters
109 ----------
110 exposure : `lsst.afw.image.Exposure`
111 Exposure to Psf-match to the reference Psf model;
112 it must return a valid PSF model via exposure.getPsf()
113 referencePsfModel : `lsst.afw.detection.Psf`
114 The Psf model to match to
115 kernelSum : `float`, optional
116 A multipicative factor to apply to the kernel sum (default=1.0)
117
118 Returns
119 -------
120 result : `struct`
121 - ``psfMatchedExposure`` : the Psf-matched Exposure.
122 This has the same parent bbox, Wcs, PhotoCalib and
123 Filter as the input Exposure but no Psf.
124 In theory the Psf should equal referencePsfModel but
125 the match is likely not exact.
126 - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel
127 - ``kernelCellSet`` : SpatialCellSet used to solve for the Psf-matching kernel
128 - ``referencePsfModel`` : Validated and/or modified reference model used
129
130 Raises
131 ------
132 RuntimeError
133 if the Exposure does not contain a Psf model
134 """
135 if not exposure.hasPsf():
136 raise RuntimeError("exposure does not contain a Psf model")
137
138 maskedImage = exposure.getMaskedImage()
139
140 self.log.info("compute Psf-matching kernel")
141 result = self._buildCellSet_buildCellSet(exposure, referencePsfModel)
142 kernelCellSet = result.kernelCellSet
143 referencePsfModel = result.referencePsfModel
144 # TODO: This should be evaluated at (or close to) the center of the
145 # exposure's bounding box in DM-32756.
146 sciAvgPos = exposure.getPsf().getAveragePosition()
147 modelAvgPos = referencePsfModel.getAveragePosition()
148 fwhmScience = exposure.getPsf().computeShape(sciAvgPos).getDeterminantRadius()*sigma2fwhm
149 fwhmModel = referencePsfModel.computeShape(modelAvgPos).getDeterminantRadius()*sigma2fwhm
150
151 basisList = makeKernelBasisList(self.kConfigkConfig, fwhmScience, fwhmModel, metadata=self.metadata)
152 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
153
154 if psfMatchingKernel.isSpatiallyVarying():
155 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
156 sParameters[0][0] = kernelSum
157 psfMatchingKernel.setSpatialParameters(sParameters)
158 else:
159 kParameters = np.array(psfMatchingKernel.getKernelParameters())
160 kParameters[0] = kernelSum
161 psfMatchingKernel.setKernelParameters(kParameters)
162
163 self.log.info("Psf-match science exposure to reference")
164 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
165 psfMatchedExposure.info.id = exposure.info.id
166 psfMatchedExposure.setFilter(exposure.getFilter())
167 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
168 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
169 psfMatchedExposure.setPsf(referencePsfModel)
170 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
171
172 # Normalize the psf-matching kernel while convolving since its magnitude is meaningless
173 # when PSF-matching one model to another.
174 convolutionControl = afwMath.ConvolutionControl()
175 convolutionControl.setDoNormalize(True)
176 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
177
178 self.log.info("done")
179 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
180 psfMatchingKernel=psfMatchingKernel,
181 kernelCellSet=kernelCellSet,
182 metadata=self.metadata,
183 )
184
185 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
186 """Print diagnostic information on spatial kernel and background fit
187
188 The debugging diagnostics are not really useful here, since the images we are matching have
189 no variance. Thus override the _diagnostic method to generate no logging information"""
190 return
191
192 def _buildCellSet(self, exposure, referencePsfModel):
193 """Build a SpatialCellSet for use with the solve method
194
195 Parameters
196 ----------
197 exposure : `lsst.afw.image.Exposure`
198 The science exposure that will be convolved; must contain a Psf
199 referencePsfModel : `lsst.afw.detection.Psf`
200 Psf model to match to
201
202 Returns
203 -------
204 result : `struct`
205 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve
206 - ``referencePsfModel`` : Validated and/or modified
207 reference model used to populate the SpatialCellSet
208
209 Notes
210 -----
211 If the reference Psf model and science Psf model have different dimensions,
212 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
213 to match that of the science Psf. If the science Psf dimensions vary across the image,
214 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
215 the dimensions to be constant.
216 """
217 sizeCellX = self.kConfig.sizeCellX
218 sizeCellY = self.kConfig.sizeCellY
219
220 scienceBBox = exposure.getBBox()
221 # Extend for proper spatial matching kernel all the way to edge, especially for narrow strips
222 scienceBBox.grow(geom.Extent2I(sizeCellX, sizeCellY))
223
224 sciencePsfModel = exposure.getPsf()
225
226 dimenR = referencePsfModel.getLocalKernel(scienceBBox.getCenter()).getDimensions()
227
228 regionSizeX, regionSizeY = scienceBBox.getDimensions()
229 scienceX0, scienceY0 = scienceBBox.getMin()
230
231 kernelCellSet = afwMath.SpatialCellSet(geom.Box2I(scienceBBox), sizeCellX, sizeCellY)
232
233 nCellX = regionSizeX//sizeCellX
234 nCellY = regionSizeY//sizeCellY
235
236 if nCellX == 0 or nCellY == 0:
237 raise ValueError("Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
238 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
239
240 # Survey the PSF dimensions of the Spatial Cell Set
241 # to identify the minimum enclosed or maximum bounding square BBox.
242 widthList = []
243 heightList = []
244 for row in range(nCellY):
245 posY = sizeCellY*row + sizeCellY//2 + scienceY0
246 for col in range(nCellX):
247 posX = sizeCellX*col + sizeCellX//2 + scienceX0
248 widthS, heightS = sciencePsfModel.computeBBox(geom.Point2D(posX, posY)).getDimensions()
249 widthList.append(widthS)
250 heightList.append(heightS)
251
252 psfSize = max(max(heightList), max(widthList))
253
254 if self.config.doAutoPadPsf:
255 minPsfSize = nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
256 paddingPix = max(0, minPsfSize - psfSize)
257 else:
258 if self.config.padPsfBy % 2 != 0:
259 raise ValueError("Config padPsfBy (%i pixels) must be even number." %
260 self.config.padPsfBy)
261 paddingPix = self.config.padPsfBy
262
263 if paddingPix > 0:
264 self.log.debug("Padding Science PSF from (%d, %d) to (%d, %d) pixels",
265 psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
266 psfSize += paddingPix
267
268 # Check that PSF is larger than the matching kernel
269 maxKernelSize = psfSize - 1
270 if maxKernelSize % 2 == 0:
271 maxKernelSize -= 1
272 if self.kConfig.kernelSize > maxKernelSize:
273 message = """
274 Kernel size (%d) too big to match Psfs of size %d.
275 Please reconfigure by setting one of the following:
276 1) kernel size to <= %d
277 2) doAutoPadPsf=True
278 3) padPsfBy to >= %s
279 """ % (self.kConfig.kernelSize, psfSize,
280 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
281 raise ValueError(message)
282
283 dimenS = geom.Extent2I(psfSize, psfSize)
284
285 if (dimenR != dimenS):
286 try:
287 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
288 self.log.info("Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
289 except Exception as e:
290 self.log.warning("Zero padding or clipping the reference PSF model of type %s and dimensions"
291 " %s to the science Psf dimensions %s because: %s",
292 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
293 dimenR = dimenS
294
295 ps = pexConfig.makePropertySet(self.kConfig)
296 for row in range(nCellY):
297 # place at center of cell
298 posY = sizeCellY*row + sizeCellY//2 + scienceY0
299
300 for col in range(nCellX):
301 # place at center of cell
302 posX = sizeCellX*col + sizeCellX//2 + scienceX0
303
304 getTraceLogger(self.log, 4).debug("Creating Psf candidate at %.1f %.1f", posX, posY)
305
306 # reference kernel image, at location of science subimage
307 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
308
309 # kernel image we are going to convolve
310 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
311
312 # The image to convolve is the science image, to the reference Psf.
313 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
314 kernelCellSet.insertCandidate(kc)
315
316 import lsstDebug
317 display = lsstDebug.Info(__name__).display
318 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
319 maskTransparency = lsstDebug.Info(__name__).maskTransparency
320 if not maskTransparency:
321 maskTransparency = 0
322 if display:
323 afwDisplay.setDefaultMaskTransparency(maskTransparency)
324 if display and displaySpatialCells:
325 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
326 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
327 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
328 title="Image to be convolved")
329 lsstDebug.frame += 1
330 return pipeBase.Struct(kernelCellSet=kernelCellSet,
331 referencePsfModel=referencePsfModel,
332 )
333
334 def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
335 """Return a MaskedImage of the a PSF Model of specified dimensions
336 """
337 rawKernel = psfModel.computeKernelImage(geom.Point2D(posX, posY)).convertF()
338 if dimensions is None:
339 dimensions = rawKernel.getDimensions()
340 if rawKernel.getDimensions() == dimensions:
341 kernelIm = rawKernel
342 else:
343 # make image of proper size
344 kernelIm = afwImage.ImageF(dimensions)
345 bboxToPlace = geom.Box2I(geom.Point2I((dimensions.getX() - rawKernel.getWidth())//2,
346 (dimensions.getY() - rawKernel.getHeight())//2),
347 rawKernel.getDimensions())
348 kernelIm.assign(rawKernel, bboxToPlace)
349
350 kernelMask = afwImage.Mask(dimensions, 0x0)
351 kernelVar = afwImage.ImageF(dimensions, 1.0)
352 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
int max
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:81
Parameters to control convolution.
A collection of SpatialCells covering an entire image.
An integer coordinate rectangle.
Definition Box.h:55
run(self, exposure, referencePsfModel, kernelSum=1.0)
_buildCellSet(self, exposure, referencePsfModel)
_diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg)
_makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None)
_solve(self, kernelCellSet, basisList, returnOnExcept=False)
Definition psfMatch.py:797
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.