21 from __future__
import absolute_import, division, print_function
23 from builtins
import range
26 from .
import diffimLib
27 import lsst.afw.geom
as afwGeom
28 import lsst.afw.image
as afwImage
29 import lsst.afw.math
as afwMath
30 import lsst.log
as log
31 import lsst.meas.algorithms
as measAlg
32 import lsst.pex.config
as pexConfig
33 import lsst.pipe.base
as pipeBase
34 from .makeKernelBasisList
import makeKernelBasisList
35 from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
36 from .
import utils
as diUtils
37 import lsst.afw.display.ds9
as ds9
39 __all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
41 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
45 nextInt = int(np.ceil(x))
46 return nextInt + 1
if nextInt % 2 == 0
else nextInt
50 """!Configuration for model-to-model Psf matching"""
52 kernel = pexConfig.ConfigChoiceField(
59 doAutoPadPsf = pexConfig.Field(
61 doc=(
"If too small, automatically pad the science Psf? "
62 "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
63 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
66 autoPadPsfTo = pexConfig.RangeField(
68 doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
69 "If the dimensions of the Psf to be matched are less than the "
70 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
71 "Ignored if doAutoPadPsf=False."),
76 padPsfBy = pexConfig.Field(
78 doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
84 self.kernel.active.singleKernelClipping =
False
85 self.kernel.active.kernelSumClipping =
False
86 self.kernel.active.spatialKernelClipping =
False
87 self.kernel.active.checkConditionNumber =
False
90 self.kernel.active.constantVarianceWeighting =
True
93 self.kernel.active.scaleByFwhm =
False
106 \anchor ModelPsfMatchTask_
108 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
110 \section ip_diffim_modelpsfmatch_Contents Contents
112 - \ref ip_diffim_modelpsfmatch_Purpose
113 - \ref ip_diffim_modelpsfmatch_Initialize
114 - \ref ip_diffim_modelpsfmatch_IO
115 - \ref ip_diffim_modelpsfmatch_Config
116 - \ref ip_diffim_modelpsfmatch_Metadata
117 - \ref ip_diffim_modelpsfmatch_Debug
118 - \ref ip_diffim_modelpsfmatch_Example
120 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
122 \section ip_diffim_modelpsfmatch_Purpose Description
124 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing
125 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates.
126 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is
127 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the
128 debugging and logging QA info should be interpreted with caution.
130 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the
131 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function,
132 leading to a loss of information around the borders. This pixel loss will be problematic for the numerical
133 stability of the kernel solution if the size of the convolution kernel (set by ModelPsfMatchConfig.kernelSize)
134 is much bigger than: psfSize//2. Thus the sizes of Psf-model matching kernels are typically smaller
135 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will
136 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that
137 needs careful attention for a given dataset.
139 The primary use case for this Task is in matching an Exposure to a constant-across-the-sky Psf model for the
140 purposes of image coaddition. It is important to note that in the code, the "template" Psf is the Psf
141 that the science image gets matched to. In this sense the order of template and science image are
142 reversed, compared to ImagePsfMatchTask, which operates on the template image.
144 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
146 \section ip_diffim_modelpsfmatch_Initialize Task initialization
148 \copydoc \_\_init\_\_
150 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
152 \section ip_diffim_modelpsfmatch_IO Invoking the Task
156 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
158 \section ip_diffim_modelpsfmatch_Config Configuration parameters
160 See \ref ModelPsfMatchConfig
162 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
164 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata
166 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask"
168 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
170 \section ip_diffim_modelpsfmatch_Debug Debug variables
172 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
173 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
174 for this Task include:
180 di = lsstDebug.getInfo(name)
181 if name == "lsst.ip.diffim.psfMatch":
182 di.display = True # global
183 di.maskTransparency = 80 # ds9 mask transparency
184 di.displayCandidates = True # show all the candidates and residuals
185 di.displayKernelBasis = False # show kernel basis functions
186 di.displayKernelMosaic = True # show kernel realized across the image
187 di.plotKernelSpatialModel = False # show coefficients of spatial model
188 di.showBadCandidates = True # show the bad candidates (red) along with good (green)
189 elif name == "lsst.ip.diffim.modelPsfMatch":
190 di.display = True # global
191 di.maskTransparency = 30 # ds9 mask transparency
192 di.displaySpatialCells = True # show spatial cells before the fit
194 lsstDebug.Info = DebugInfo
198 Note that if you want addional logging info, you may add to your scripts:
200 import lsst.log.utils as logUtils
201 logUtils.traceSetAt("ip.diffim", 4)
204 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
206 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask
208 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g.
210 examples/modelPsfMatchTask.py
211 examples/modelPsfMatchTask.py --debug
212 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits
215 \dontinclude modelPsfMatchTask.py
216 Create a subclass of ModelPsfMatchTask that accepts two exposures. Note that the "template" exposure
217 contains the Psf that will get matched to, and the "science" exposure is the one that will be convolved:
218 \skip MyModelPsfMatchTask
221 And allow the user the freedom to either run the script in default mode, or point to their own images on disk.
222 Note that these images must be readable as an lsst.afw.image.Exposure:
226 We have enabled some minor display debugging in this script via the --debug option. However, if you
227 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following
228 block checks for this script:
232 \dontinclude modelPsfMatchTask.py
233 Finally, we call a run method that we define below. First set up a Config and modify some of the parameters.
234 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, since we are operating with
235 fixed--size images (i.e. the size of the input Psf models).
239 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images
240 are sent, make some fake data up for the sake of this example script (have a look at the code if you want
241 more details on generateFakeData):
245 Display the two images if --debug:
249 Create and run the Task:
253 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
255 @until result.psfMatchedExposure
257 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
260 ConfigClass = ModelPsfMatchConfig
263 """!Create a ModelPsfMatchTask
265 \param *args arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
266 \param **kwargs keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
268 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
269 does have a run() method, which is the default way to call the Task.
271 PsfMatchTask.__init__(self, *args, **kwargs)
275 def run(self, exposure, referencePsfModel, kernelSum=1.0):
276 """!Psf-match an exposure to a model Psf
278 @param exposure: Exposure to Psf-match to the reference Psf model;
279 it must return a valid PSF model via exposure.getPsf()
280 @param referencePsfModel: The Psf model to match to (an lsst.afw.detection.Psf)
281 @param kernelSum: A multipicative factor to apply to the kernel sum (default=1.0)
284 - psfMatchedExposure: the Psf-matched Exposure. This has the same parent bbox, Wcs, Calib and
285 Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but
286 the match is likely not exact.
287 - psfMatchingKernel: the spatially varying Psf-matching kernel
288 - kernelCellSet: SpatialCellSet used to solve for the Psf-matching kernel
289 - referencePsfModel: Validated and/or modified reference model used
291 Raise a RuntimeError if the Exposure does not contain a Psf model
293 if not exposure.hasPsf():
294 raise RuntimeError(
"exposure does not contain a Psf model")
296 maskedImage = exposure.getMaskedImage()
298 self.log.info(
"compute Psf-matching kernel")
300 kernelCellSet = result.kernelCellSet
301 referencePsfModel = result.referencePsfModel
302 width, height = referencePsfModel.getLocalKernel().getDimensions()
303 psfAttr1 = measAlg.PsfAttributes(exposure.getPsf(), width//2, height//2)
304 psfAttr2 = measAlg.PsfAttributes(referencePsfModel, width//2, height//2)
305 s1 = psfAttr1.computeGaussianWidth(psfAttr1.ADAPTIVE_MOMENT)
306 s2 = psfAttr2.computeGaussianWidth(psfAttr2.ADAPTIVE_MOMENT)
307 fwhm1 = s1 * sigma2fwhm
308 fwhm2 = s2 * sigma2fwhm
311 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
313 if psfMatchingKernel.isSpatiallyVarying():
314 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
315 sParameters[0][0] = kernelSum
316 psfMatchingKernel.setSpatialParameters(sParameters)
318 kParameters = np.array(psfMatchingKernel.getKernelParameters())
319 kParameters[0] = kernelSum
320 psfMatchingKernel.setKernelParameters(kParameters)
322 self.log.info(
"Psf-match science exposure to reference")
323 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
324 psfMatchedExposure.setFilter(exposure.getFilter())
325 psfMatchedExposure.setCalib(exposure.getCalib())
326 psfMatchedExposure.setPsf(referencePsfModel)
327 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
332 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
334 self.log.info(
"done")
335 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
336 psfMatchingKernel=psfMatchingKernel,
337 kernelCellSet=kernelCellSet,
338 metadata=self.metadata,
341 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
342 """!Print diagnostic information on spatial kernel and background fit
344 The debugging diagnostics are not really useful here, since the images we are matching have
345 no variance. Thus override the _diagnostic method to generate no logging information"""
349 """!Build a SpatialCellSet for use with the solve method
351 @param exposure: The science exposure that will be convolved; must contain a Psf
352 @param referencePsfModel: Psf model to match to
355 -kernelCellSet: a SpatialCellSet to be used by self._solve
356 -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet
358 If the reference Psf model and science Psf model have different dimensions,
359 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
360 to match that of the science Psf. If the science Psf dimensions vary across the image,
361 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
362 the dimensions to be constant.
364 scienceBBox = exposure.getBBox()
365 sciencePsfModel = exposure.getPsf()
367 dimenR = referencePsfModel.getLocalKernel().getDimensions()
368 psfWidth, psfHeight = dimenR
370 regionSizeX, regionSizeY = scienceBBox.getDimensions()
371 scienceX0, scienceY0 = scienceBBox.getMin()
373 sizeCellX = self.kConfig.sizeCellX
374 sizeCellY = self.kConfig.sizeCellY
376 kernelCellSet = afwMath.SpatialCellSet(
377 afwGeom.Box2I(afwGeom.Point2I(scienceX0, scienceY0),
378 afwGeom.Extent2I(regionSizeX, regionSizeY)),
382 nCellX = regionSizeX//sizeCellX
383 nCellY = regionSizeY//sizeCellY
385 if nCellX == 0
or nCellY == 0:
386 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
387 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
393 for row
in range(nCellY):
394 posY = sizeCellY*row + sizeCellY//2 + scienceY0
395 for col
in range(nCellX):
396 posX = sizeCellX*col + sizeCellX//2 + scienceX0
397 widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
398 widthList.append(widthS)
399 heightList.append(heightS)
401 psfSize = max(max(heightList), max(widthList))
403 if self.config.doAutoPadPsf:
404 minPsfSize =
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
405 paddingPix = max(0, minPsfSize - psfSize)
407 if self.config.padPsfBy % 2 != 0:
408 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
409 self.config.padPsfBy)
410 paddingPix = self.config.padPsfBy
413 self.log.info(
"Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
414 (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
415 psfSize += paddingPix
418 maxKernelSize = psfSize - 1
419 if maxKernelSize % 2 == 0:
421 if self.kConfig.kernelSize > maxKernelSize:
423 Kernel size (%d) too big to match Psfs of size %d.
424 Please reconfigure by setting one of the following:
425 1) kernel size to <= %d
428 """ % (self.kConfig.kernelSize, psfSize,
429 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
430 raise ValueError(message)
432 dimenS = afwGeom.Extent2I(psfSize, psfSize)
434 if (dimenR != dimenS):
435 self.log.info(
"Adjusting dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
436 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
439 policy = pexConfig.makePolicy(self.kConfig)
440 for row
in range(nCellY):
442 posY = sizeCellY * row + sizeCellY//2 + scienceY0
444 for col
in range(nCellX):
446 posX = sizeCellX * col + sizeCellX//2 + scienceX0
448 log.log(
"TRACE4." + self.log.getName(), log.DEBUG,
449 "Creating Psf candidate at %.1f %.1f", posX, posY)
452 kernelImageR = referencePsfModel.computeImage(afwGeom.Point2D(posX, posY)).convertF()
453 kernelMaskR = afwImage.MaskU(dimenR)
455 kernelVarR = afwImage.ImageF(dimenR)
457 referenceMI = afwImage.MaskedImageF(kernelImageR, kernelMaskR, kernelVarR)
460 rawKernel = sciencePsfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
461 if rawKernel.getDimensions() == dimenR:
462 kernelImageS = rawKernel
465 kernelImageS = afwImage.ImageF(dimenR)
466 bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((psfSize - rawKernel.getWidth())//2,
467 (psfSize - rawKernel.getHeight())//2),
468 rawKernel.getDimensions())
469 kernelImageS.assign(rawKernel, bboxToPlace)
471 kernelMaskS = afwImage.MaskU(dimenS)
473 kernelVarS = afwImage.ImageF(dimenS)
475 scienceMI = afwImage.MaskedImageF(kernelImageS, kernelMaskS, kernelVarS)
478 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
479 kernelCellSet.insertCandidate(kc)
482 display = lsstDebug.Info(__name__).display
483 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
484 maskTransparency = lsstDebug.Info(__name__).maskTransparency
485 if not maskTransparency:
488 ds9.setMaskTransparency(maskTransparency)
489 if display
and displaySpatialCells:
490 diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
491 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
492 size=4, frame=lsstDebug.frame, title=
"Image to be convolved")
494 return pipeBase.Struct(kernelCellSet=kernelCellSet,
495 referencePsfModel=referencePsfModel,
def _diagnostic
Print diagnostic information on spatial kernel and background fit.
def __init__
Create a ModelPsfMatchTask.
Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure...
Configuration for model-to-model Psf matching.
def _buildCellSet
Build a SpatialCellSet for use with the solve method.
def run
Psf-match an exposure to a model Psf.