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.pex.config
as pexConfig
32 import lsst.pipe.base
as pipeBase
33 from .makeKernelBasisList
import makeKernelBasisList
34 from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
35 from .
import utils
as diUtils
36 import lsst.afw.display.ds9
as ds9
38 __all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
40 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
44 nextInt = int(np.ceil(x))
45 return nextInt + 1
if nextInt % 2 == 0
else nextInt
49 """!Configuration for model-to-model Psf matching"""
51 kernel = pexConfig.ConfigChoiceField(
58 doAutoPadPsf = pexConfig.Field(
60 doc=(
"If too small, automatically pad the science Psf? "
61 "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
62 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
65 autoPadPsfTo = pexConfig.RangeField(
67 doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
68 "If the dimensions of the Psf to be matched are less than the "
69 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
70 "Ignored if doAutoPadPsf=False."),
75 padPsfBy = pexConfig.Field(
77 doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
83 self.kernel.active.singleKernelClipping =
False
84 self.kernel.active.kernelSumClipping =
False
85 self.kernel.active.spatialKernelClipping =
False
86 self.kernel.active.checkConditionNumber =
False
89 self.kernel.active.constantVarianceWeighting =
True
92 self.kernel.active.scaleByFwhm =
False
105 \anchor ModelPsfMatchTask_
107 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
109 \section ip_diffim_modelpsfmatch_Contents Contents
111 - \ref ip_diffim_modelpsfmatch_Purpose
112 - \ref ip_diffim_modelpsfmatch_Initialize
113 - \ref ip_diffim_modelpsfmatch_IO
114 - \ref ip_diffim_modelpsfmatch_Config
115 - \ref ip_diffim_modelpsfmatch_Metadata
116 - \ref ip_diffim_modelpsfmatch_Debug
117 - \ref ip_diffim_modelpsfmatch_Example
119 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
121 \section ip_diffim_modelpsfmatch_Purpose Description
123 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing
124 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates.
125 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is
126 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the
127 debugging and logging QA info should be interpreted with caution.
129 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the
130 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function,
131 leading to a loss of information around the borders. This pixel loss will be problematic for the numerical
132 stability of the kernel solution if the size of the convolution kernel (set by ModelPsfMatchConfig.kernelSize)
133 is much bigger than: psfSize//2. Thus the sizes of Psf-model matching kernels are typically smaller
134 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will
135 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that
136 needs careful attention for a given dataset.
138 The primary use case for this Task is in matching an Exposure to a constant-across-the-sky Psf model for the
139 purposes of image coaddition. It is important to note that in the code, the "template" Psf is the Psf
140 that the science image gets matched to. In this sense the order of template and science image are
141 reversed, compared to ImagePsfMatchTask, which operates on the template image.
143 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
145 \section ip_diffim_modelpsfmatch_Initialize Task initialization
147 \copydoc \_\_init\_\_
149 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
151 \section ip_diffim_modelpsfmatch_IO Invoking the Task
155 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
157 \section ip_diffim_modelpsfmatch_Config Configuration parameters
159 See \ref ModelPsfMatchConfig
161 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
163 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata
165 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask"
167 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
169 \section ip_diffim_modelpsfmatch_Debug Debug variables
171 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
172 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
173 for this Task include:
179 di = lsstDebug.getInfo(name)
180 if name == "lsst.ip.diffim.psfMatch":
181 di.display = True # global
182 di.maskTransparency = 80 # ds9 mask transparency
183 di.displayCandidates = True # show all the candidates and residuals
184 di.displayKernelBasis = False # show kernel basis functions
185 di.displayKernelMosaic = True # show kernel realized across the image
186 di.plotKernelSpatialModel = False # show coefficients of spatial model
187 di.showBadCandidates = True # show the bad candidates (red) along with good (green)
188 elif name == "lsst.ip.diffim.modelPsfMatch":
189 di.display = True # global
190 di.maskTransparency = 30 # ds9 mask transparency
191 di.displaySpatialCells = True # show spatial cells before the fit
193 lsstDebug.Info = DebugInfo
197 Note that if you want addional logging info, you may add to your scripts:
199 import lsst.log.utils as logUtils
200 logUtils.traceSetAt("ip.diffim", 4)
203 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
205 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask
207 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g.
209 examples/modelPsfMatchTask.py
210 examples/modelPsfMatchTask.py --debug
211 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits
214 \dontinclude modelPsfMatchTask.py
215 Create a subclass of ModelPsfMatchTask that accepts two exposures. Note that the "template" exposure
216 contains the Psf that will get matched to, and the "science" exposure is the one that will be convolved:
217 \skip MyModelPsfMatchTask
220 And allow the user the freedom to either run the script in default mode, or point to their own images on disk.
221 Note that these images must be readable as an lsst.afw.image.Exposure:
225 We have enabled some minor display debugging in this script via the --debug option. However, if you
226 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following
227 block checks for this script:
231 \dontinclude modelPsfMatchTask.py
232 Finally, we call a run method that we define below. First set up a Config and modify some of the parameters.
233 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, since we are operating with
234 fixed--size images (i.e. the size of the input Psf models).
238 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images
239 are sent, make some fake data up for the sake of this example script (have a look at the code if you want
240 more details on generateFakeData):
244 Display the two images if --debug:
248 Create and run the Task:
252 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
254 @until result.psfMatchedExposure
256 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
259 ConfigClass = ModelPsfMatchConfig
262 """!Create a ModelPsfMatchTask
264 \param *args arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
265 \param **kwargs keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
267 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
268 does have a run() method, which is the default way to call the Task.
270 PsfMatchTask.__init__(self, *args, **kwargs)
274 def run(self, exposure, referencePsfModel, kernelSum=1.0):
275 """!Psf-match an exposure to a model Psf
277 @param exposure: Exposure to Psf-match to the reference Psf model;
278 it must return a valid PSF model via exposure.getPsf()
279 @param referencePsfModel: The Psf model to match to (an lsst.afw.detection.Psf)
280 @param kernelSum: A multipicative factor to apply to the kernel sum (default=1.0)
283 - psfMatchedExposure: the Psf-matched Exposure. This has the same parent bbox, Wcs, Calib and
284 Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but
285 the match is likely not exact.
286 - psfMatchingKernel: the spatially varying Psf-matching kernel
287 - kernelCellSet: SpatialCellSet used to solve for the Psf-matching kernel
288 - referencePsfModel: Validated and/or modified reference model used
290 Raise a RuntimeError if the Exposure does not contain a Psf model
292 if not exposure.hasPsf():
293 raise RuntimeError(
"exposure does not contain a Psf model")
295 maskedImage = exposure.getMaskedImage()
297 self.log.info(
"compute Psf-matching kernel")
299 kernelCellSet = result.kernelCellSet
300 referencePsfModel = result.referencePsfModel
301 fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius() * sigma2fwhm
302 fwhmModel = referencePsfModel.computeShape().getDeterminantRadius() * sigma2fwhm
305 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
307 if psfMatchingKernel.isSpatiallyVarying():
308 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
309 sParameters[0][0] = kernelSum
310 psfMatchingKernel.setSpatialParameters(sParameters)
312 kParameters = np.array(psfMatchingKernel.getKernelParameters())
313 kParameters[0] = kernelSum
314 psfMatchingKernel.setKernelParameters(kParameters)
316 self.log.info(
"Psf-match science exposure to reference")
317 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
318 psfMatchedExposure.setFilter(exposure.getFilter())
319 psfMatchedExposure.setCalib(exposure.getCalib())
320 psfMatchedExposure.setPsf(referencePsfModel)
321 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
326 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
328 self.log.info(
"done")
329 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
330 psfMatchingKernel=psfMatchingKernel,
331 kernelCellSet=kernelCellSet,
332 metadata=self.metadata,
335 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
336 """!Print diagnostic information on spatial kernel and background fit
338 The debugging diagnostics are not really useful here, since the images we are matching have
339 no variance. Thus override the _diagnostic method to generate no logging information"""
343 """!Build a SpatialCellSet for use with the solve method
345 @param exposure: The science exposure that will be convolved; must contain a Psf
346 @param referencePsfModel: Psf model to match to
349 -kernelCellSet: a SpatialCellSet to be used by self._solve
350 -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet
352 If the reference Psf model and science Psf model have different dimensions,
353 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
354 to match that of the science Psf. If the science Psf dimensions vary across the image,
355 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
356 the dimensions to be constant.
358 scienceBBox = exposure.getBBox()
359 sciencePsfModel = exposure.getPsf()
361 dimenR = referencePsfModel.getLocalKernel().getDimensions()
362 psfWidth, psfHeight = dimenR
364 regionSizeX, regionSizeY = scienceBBox.getDimensions()
365 scienceX0, scienceY0 = scienceBBox.getMin()
367 sizeCellX = self.kConfig.sizeCellX
368 sizeCellY = self.kConfig.sizeCellY
370 kernelCellSet = afwMath.SpatialCellSet(
371 afwGeom.Box2I(afwGeom.Point2I(scienceX0, scienceY0),
372 afwGeom.Extent2I(regionSizeX, regionSizeY)),
376 nCellX = regionSizeX//sizeCellX
377 nCellY = regionSizeY//sizeCellY
379 if nCellX == 0
or nCellY == 0:
380 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
381 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
387 for row
in range(nCellY):
388 posY = sizeCellY*row + sizeCellY//2 + scienceY0
389 for col
in range(nCellX):
390 posX = sizeCellX*col + sizeCellX//2 + scienceX0
391 widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
392 widthList.append(widthS)
393 heightList.append(heightS)
395 psfSize = max(max(heightList), max(widthList))
397 if self.config.doAutoPadPsf:
398 minPsfSize =
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
399 paddingPix = max(0, minPsfSize - psfSize)
401 if self.config.padPsfBy % 2 != 0:
402 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
403 self.config.padPsfBy)
404 paddingPix = self.config.padPsfBy
407 self.log.info(
"Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
408 (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
409 psfSize += paddingPix
412 maxKernelSize = psfSize - 1
413 if maxKernelSize % 2 == 0:
415 if self.kConfig.kernelSize > maxKernelSize:
417 Kernel size (%d) too big to match Psfs of size %d.
418 Please reconfigure by setting one of the following:
419 1) kernel size to <= %d
422 """ % (self.kConfig.kernelSize, psfSize,
423 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
424 raise ValueError(message)
426 dimenS = afwGeom.Extent2I(psfSize, psfSize)
428 if (dimenR != dimenS):
430 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
431 self.log.info(
"Adjusted dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
432 except Exception
as e:
433 self.log.warn(
"Zero padding or clipping the reference PSF model of type %s and dimensions %s"
434 " to the science Psf dimensions %s because: %s",
435 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
438 policy = pexConfig.makePolicy(self.kConfig)
439 for row
in range(nCellY):
441 posY = sizeCellY * row + sizeCellY//2 + scienceY0
443 for col
in range(nCellX):
445 posX = sizeCellX * col + sizeCellX//2 + scienceX0
447 log.log(
"TRACE4." + self.log.getName(), log.DEBUG,
448 "Creating Psf candidate at %.1f %.1f", posX, posY)
451 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
454 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
457 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
458 kernelCellSet.insertCandidate(kc)
461 display = lsstDebug.Info(__name__).display
462 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
463 maskTransparency = lsstDebug.Info(__name__).maskTransparency
464 if not maskTransparency:
467 ds9.setMaskTransparency(maskTransparency)
468 if display
and displaySpatialCells:
469 diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
470 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
471 size=4, frame=lsstDebug.frame, title=
"Image to be convolved")
473 return pipeBase.Struct(kernelCellSet=kernelCellSet,
474 referencePsfModel=referencePsfModel,
478 """! Return a MaskedImage of the a PSF Model of specified dimensions
480 rawKernel = psfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
481 if dimensions
is None:
482 dimensions = rawKernel.getDimensions()
483 if rawKernel.getDimensions() == dimensions:
487 kernelIm = afwImage.ImageF(dimensions)
488 bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((dimensions.getX() - rawKernel.getWidth())//2,
489 (dimensions.getY() - rawKernel.getHeight())//2),
490 rawKernel.getDimensions())
491 kernelIm.assign(rawKernel, bboxToPlace)
493 kernelMask = afwImage.Mask(dimensions, 0x0)
494 kernelVar = afwImage.ImageF(dimensions, 1.0)
495 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
def _diagnostic
Print diagnostic information on spatial kernel and background fit.
def _makePsfMaskedImage
Return a MaskedImage of the a PSF Model of specified dimensions.
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.