LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
modelPsfMatch.py
Go to the documentation of this file.
1 from __future__ import absolute_import
2 from builtins import range
3 # LSST Data Management System
4 # Copyright 2008-2016 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 #
23 import numpy as num
24 from . import diffimLib
25 import lsst.afw.geom as afwGeom
26 import lsst.afw.image as afwImage
27 import lsst.afw.math as afwMath
28 import lsst.log as log
29 import lsst.meas.algorithms as measAlg
30 import lsst.pex.config as pexConfig
31 import lsst.pipe.base as pipeBase
32 from .makeKernelBasisList import makeKernelBasisList
33 from .psfMatch import PsfMatchTask, PsfMatchConfigAL
34 from . import utils as diUtils
35 import lsst.afw.display.ds9 as ds9
36 
37 sigma2fwhm = 2. * num.sqrt(2. * num.log(2.))
38 
39 
40 class ModelPsfMatchConfig(pexConfig.Config):
41  """!Configuration for model-to-model Psf matching"""
42 
43  kernel = pexConfig.ConfigChoiceField(
44  doc="kernel type",
45  typemap=dict(
46  AL=PsfMatchConfigAL,
47  ),
48  default="AL",
49  )
50 
51  padPsf = pexConfig.Field(
52  dtype=bool,
53  doc="Grow PSF dimensions to largest PSF dimension on input exposure; else clip to smallest",
54  default=False,
55  )
56 
57  def setDefaults(self):
58  # No sigma clipping
59  self.kernel.active.singleKernelClipping = False
60  self.kernel.active.kernelSumClipping = False
61  self.kernel.active.spatialKernelClipping = False
62  self.kernel.active.checkConditionNumber = False
63 
64  # Variance is ill defined
65  self.kernel.active.constantVarianceWeighting = True
66 
67  # Psfs are typically small; reduce the kernel size
68  self.kernel.active.kernelSizeMin = 11
69  self.kernel.active.kernelSize = 11
70 
71 ## \addtogroup LSST_task_documentation
72 ## \{
73 ## \page ModelPsfMatchTask
74 ## \ref ModelPsfMatchTask_ "ModelPsfMatchTask"
75 ## \copybrief ModelPsfMatchTask
76 ## \}
77 
78 
79 class ModelPsfMatchTask(PsfMatchTask):
80  """!
81 \anchor ModelPsfMatchTask_
82 
83 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
84 
85 \section ip_diffim_modelpsfmatch_Contents Contents
86 
87  - \ref ip_diffim_modelpsfmatch_Purpose
88  - \ref ip_diffim_modelpsfmatch_Initialize
89  - \ref ip_diffim_modelpsfmatch_IO
90  - \ref ip_diffim_modelpsfmatch_Config
91  - \ref ip_diffim_modelpsfmatch_Metadata
92  - \ref ip_diffim_modelpsfmatch_Debug
93  - \ref ip_diffim_modelpsfmatch_Example
94 
95 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
96 
97 \section ip_diffim_modelpsfmatch_Purpose Description
98 
99 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing
100 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates.
101 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is
102 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the
103 debugging and logging QA info should be interpreted with caution.
104 
105 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the
106 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function,
107 leading to a loss of information around the borders. This pixel loss will be problematic for the numerical
108 stability of the kernel solution if the size of the convolution kernel (set by ModelPsfMatchConfig.kernelSize)
109 is much bigger than: psfSize//2. Thus the sizes of Psf-model matching kernels are typically smaller
110 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will
111 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that
112 needs careful attention for a given dataset.
113 
114 The primary use case for this Task is in matching an Exposure to a constant-across-the-sky Psf model for the
115 purposes of image coaddition. It is important to note that in the code, the "template" Psf is the Psf
116 that the science image gets matched to. In this sense the order of template and science image are
117 reversed, compared to ImagePsfMatchTask, which operates on the template image.
118 
119 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
120 
121 \section ip_diffim_modelpsfmatch_Initialize Task initialization
122 
123 \copydoc \_\_init\_\_
124 
125 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
126 
127 \section ip_diffim_modelpsfmatch_IO Invoking the Task
128 
129 \copydoc run
130 
131 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
132 
133 \section ip_diffim_modelpsfmatch_Config Configuration parameters
134 
135 See \ref ModelPsfMatchConfig
136 
137 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
138 
139 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata
140 
141 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask"
142 
143 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
144 
145 \section ip_diffim_modelpsfmatch_Debug Debug variables
146 
147 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
148 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
149 for this Task include:
150 
151 \code{.py}
152  import sys
153  import lsstDebug
154  def DebugInfo(name):
155  di = lsstDebug.getInfo(name)
156  if name == "lsst.ip.diffim.psfMatch":
157  di.display = True # global
158  di.maskTransparency = 80 # ds9 mask transparency
159  di.displayCandidates = True # show all the candidates and residuals
160  di.displayKernelBasis = False # show kernel basis functions
161  di.displayKernelMosaic = True # show kernel realized across the image
162  di.plotKernelSpatialModel = False # show coefficients of spatial model
163  di.showBadCandidates = True # show the bad candidates (red) along with good (green)
164  elif name == "lsst.ip.diffim.modelPsfMatch":
165  di.display = True # global
166  di.maskTransparency = 30 # ds9 mask transparency
167  di.displaySpatialCells = True # show spatial cells before the fit
168  return di
169  lsstDebug.Info = DebugInfo
170  lsstDebug.frame = 1
171 \endcode
172 
173 Note that if you want addional logging info, you may add to your scripts:
174 \code{.py}
175 import lsst.log.utils as logUtils
176 logUtils.traceSetAt("ip.diffim", 4)
177 \endcode
178 
179 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
180 
181 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask
182 
183 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g.
184 \code
185 examples/modelPsfMatchTask.py
186 examples/modelPsfMatchTask.py --debug
187 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits
188 \endcode
189 
190 \dontinclude modelPsfMatchTask.py
191 Create a subclass of ModelPsfMatchTask that accepts two exposures. Note that the "template" exposure
192 contains the Psf that will get matched to, and the "science" exposure is the one that will be convolved:
193 \skip MyModelPsfMatchTask
194 @until return
195 
196 And allow the user the freedom to either run the script in default mode, or point to their own images on disk.
197 Note that these images must be readable as an lsst.afw.image.Exposure:
198 \skip main
199 @until parse_args
200 
201 We have enabled some minor display debugging in this script via the --debug option. However, if you
202 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following
203 block checks for this script:
204 \skip args.debug
205 @until sys.stderr
206 
207 \dontinclude modelPsfMatchTask.py
208 Finally, we call a run method that we define below. First set up a Config and modify some of the parameters.
209 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, since we are operating with
210 fixed--size images (i.e. the size of the input Psf models).
211 \skip run(args)
212 @until False
213 
214 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images
215 are sent, make some fake data up for the sake of this example script (have a look at the code if you want
216 more details on generateFakeData):
217 \skip requested
218 @until sizeCellY
219 
220 Display the two images if --debug:
221 \skip args.debug
222 @until Science
223 
224 Create and run the Task:
225 \skip Create
226 @until result
227 
228 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
229 \skip args.debug
230 @until result.psfMatchedExposure
231 
232 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
233 
234  """
235  ConfigClass = ModelPsfMatchConfig
236 
237  def __init__(self, *args, **kwargs):
238  """!Create a ModelPsfMatchTask
239 
240  \param *args arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
241  \param **kwargs keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
242 
243  Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
244  does have a run() method, which is the default way to call the Task.
245  """
246  PsfMatchTask.__init__(self, *args, **kwargs)
247  self.kConfig = self.config.kernel.active
248 
249  @pipeBase.timeMethod
250  def run(self, exposure, referencePsfModel, kernelSum=1.0):
251  """!Psf-match an exposure to a model Psf
252 
253  @param exposure: Exposure to Psf-match to the reference Psf model;
254  it must return a valid PSF model via exposure.getPsf()
255  @param referencePsfModel: The Psf model to match to (an lsst.afw.detection.Psf)
256  @param kernelSum: A multipicative factor to apply to the kernel sum (default=1.0)
257 
258  @return
259  - psfMatchedExposure: the Psf-matched Exposure. This has the same parent bbox, Wcs, Calib and
260  Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but
261  the match is likely not exact.
262  - psfMatchingKernel: the spatially varying Psf-matching kernel
263  - kernelCellSet: SpatialCellSet used to solve for the Psf-matching kernel
264  - referencePsfModel: Validated and/or modified reference model used
265 
266  Raise a RuntimeError if the Exposure does not contain a Psf model
267  """
268  if not exposure.hasPsf():
269  raise RuntimeError("exposure does not contain a Psf model")
270 
271  maskedImage = exposure.getMaskedImage()
272 
273  self.log.info("compute Psf-matching kernel")
274  result = self._buildCellSet(exposure, referencePsfModel)
275  kernelCellSet = result.kernelCellSet
276  referencePsfModel = result.referencePsfModel
277  width, height = referencePsfModel.getLocalKernel().getDimensions()
278  psfAttr1 = measAlg.PsfAttributes(exposure.getPsf(), width//2, height//2)
279  psfAttr2 = measAlg.PsfAttributes(referencePsfModel, width//2, height//2)
280  s1 = psfAttr1.computeGaussianWidth(psfAttr1.ADAPTIVE_MOMENT) # gaussian sigma in pixels
281  s2 = psfAttr2.computeGaussianWidth(psfAttr2.ADAPTIVE_MOMENT) # gaussian sigma in pixels
282  fwhm1 = s1 * sigma2fwhm # science Psf
283  fwhm2 = s2 * sigma2fwhm # template Psf
284 
285  basisList = makeKernelBasisList(self.kConfig, fwhm1, fwhm2, metadata=self.metadata)
286  spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
287 
288  if psfMatchingKernel.isSpatiallyVarying():
289  sParameters = num.array(psfMatchingKernel.getSpatialParameters())
290  sParameters[0][0] = kernelSum
291  psfMatchingKernel.setSpatialParameters(sParameters)
292  else:
293  kParameters = num.array(psfMatchingKernel.getKernelParameters())
294  kParameters[0] = kernelSum
295  psfMatchingKernel.setKernelParameters(kParameters)
296 
297  self.log.info("Psf-match science exposure to reference")
298  psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
299  psfMatchedExposure.setFilter(exposure.getFilter())
300  psfMatchedExposure.setCalib(exposure.getCalib())
301  psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
302 
303  # Normalize the psf-matching kernel while convolving since its magnitude is meaningless
304  # when PSF-matching one model to another.
305  doNormalize = True
306  afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
307 
308  self.log.info("done")
309  return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
310  psfMatchingKernel=psfMatchingKernel,
311  kernelCellSet=kernelCellSet,
312  metadata=self.metadata,
313  referencePsfModel=referencePsfModel,
314  )
315 
316  def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
317  """!Print diagnostic information on spatial kernel and background fit
318 
319  The debugging diagnostics are not really useful here, since the images we are matching have
320  no variance. Thus override the _diagnostic method to generate no logging information"""
321  return
322 
323  def _buildCellSet(self, exposure, referencePsfModel):
324  """!Build a SpatialCellSet for use with the solve method
325 
326  @param exposure: The science exposure that will be convolved; must contain a Psf
327  @param referencePsfModel: Psf model to match to
328 
329  @return
330  -kernelCellSet: a SpatialCellSet to be used by self._solve
331  -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet
332 
333  If the reference Psf model and science Psf model have different dimensions,
334  adjust the referencePsfModel (the model to which the exposure PSF will be matched)
335  to match that of the science Psf. If the science Psf dimensions vary across the image,
336  as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
337  the dimensions to be constant.
338  """
339  scienceBBox = exposure.getBBox()
340  sciencePsfModel = exposure.getPsf()
341 
342  dimenR = referencePsfModel.getLocalKernel().getDimensions()
343  psfWidth, psfHeight = dimenR
344 
345  maxKernelSize = min(psfWidth, psfHeight) - 1
346  if maxKernelSize % 2 == 0:
347  maxKernelSize -= 1
348  if self.kConfig.kernelSize > maxKernelSize:
349  raise ValueError("Kernel size (%d) too big to match Psfs of size %d; reduce to at least %d" % (
350  self.kConfig.kernelSize, psfWidth, maxKernelSize))
351 
352  regionSizeX, regionSizeY = scienceBBox.getDimensions()
353  scienceX0, scienceY0 = scienceBBox.getMin()
354 
355  sizeCellX = self.kConfig.sizeCellX
356  sizeCellY = self.kConfig.sizeCellY
357 
358  kernelCellSet = afwMath.SpatialCellSet(
359  afwGeom.Box2I(afwGeom.Point2I(scienceX0, scienceY0),
360  afwGeom.Extent2I(regionSizeX, regionSizeY)),
361  sizeCellX, sizeCellY
362  )
363 
364  nCellX = regionSizeX//sizeCellX
365  nCellY = regionSizeY//sizeCellY
366 
367  # Survey the PSF dimensions of the Spatial Cell Set
368  # to identify the minimum enclosed or maximum bounding square BBox.
369  # This loop can be made faster with a cheaper method to inspect PSF dimensions
370  widthList = []
371  heightList = []
372  for row in range(nCellY):
373  posY = sizeCellY*row + sizeCellY//2 + scienceY0
374  for col in range(nCellX):
375  posX = sizeCellX*col + sizeCellX//2 + scienceX0
376  widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
377  widthList.append(widthS)
378  heightList.append(heightS)
379 
380  lenMax = max(max(heightList), max(widthList))
381  lenMin = min(min(heightList), min(widthList))
382 
383  lenPsfScience = lenMax if self.config.padPsf else lenMin
384  dimenS = afwGeom.Extent2I(lenPsfScience, lenPsfScience)
385 
386  if (dimenR != dimenS):
387  self.log.info("Adjusting dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
388  referencePsfModel = referencePsfModel.resized(lenPsfScience, lenPsfScience)
389  dimenR = dimenS
390 
391  policy = pexConfig.makePolicy(self.kConfig)
392  for row in range(nCellY):
393  # place at center of cell
394  posY = sizeCellY * row + sizeCellY//2 + scienceY0
395 
396  for col in range(nCellX):
397  # place at center of cell
398  posX = sizeCellX * col + sizeCellX//2 + scienceX0
399 
400  log.log("TRACE4." + self.log.getName(), log.DEBUG,
401  "Creating Psf candidate at %.1f %.1f", posX, posY)
402 
403  # reference kernel image, at location of science subimage
404  kernelImageR = referencePsfModel.computeImage(afwGeom.Point2D(posX, posY)).convertF()
405  kernelMaskR = afwImage.MaskU(dimenR)
406  kernelMaskR.set(0)
407  kernelVarR = afwImage.ImageF(dimenR)
408  kernelVarR.set(1.0)
409  referenceMI = afwImage.MaskedImageF(kernelImageR, kernelMaskR, kernelVarR)
410 
411  # kernel image we are going to convolve
412  rawKernel = sciencePsfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
413  if rawKernel.getDimensions() == dimenR:
414  kernelImageS = rawKernel
415  else:
416  # make image of proper size
417  kernelImageS = afwImage.ImageF(dimenR)
418  if self.config.padPsf:
419  bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((lenMax - rawKernel.getWidth())//2,
420  (lenMax - rawKernel.getHeight())//2),
421  rawKernel.getDimensions())
422  kernelImageS.assign(rawKernel, bboxToPlace)
423  else:
424  bboxToExtract = afwGeom.Box2I(afwGeom.Point2I((rawKernel.getWidth() - lenMin)//2,
425  (rawKernel.getHeight() - lenMin)//2),
426  dimenR)
427  rawKernel.setXY0(0, 0)
428  kernelImageS = rawKernel.Factory(rawKernel, bboxToExtract)
429 
430  kernelMaskS = afwImage.MaskU(dimenS)
431  kernelMaskS.set(0)
432  kernelVarS = afwImage.ImageF(dimenS)
433  kernelVarS.set(1.0)
434  scienceMI = afwImage.MaskedImageF(kernelImageS, kernelMaskS, kernelVarS)
435 
436  # The image to convolve is the science image, to the reference Psf.
437  kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
438  kernelCellSet.insertCandidate(kc)
439 
440  import lsstDebug
441  display = lsstDebug.Info(__name__).display
442  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
443  maskTransparency = lsstDebug.Info(__name__).maskTransparency
444  if not maskTransparency:
445  maskTransparency = 0
446  if display:
447  ds9.setMaskTransparency(maskTransparency)
448  if display and displaySpatialCells:
449  diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
450  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
451  size=4, frame=lsstDebug.frame, title="Image to be convolved")
452  lsstDebug.frame += 1
453  return pipeBase.Struct(kernelCellSet=kernelCellSet,
454  referencePsfModel=referencePsfModel,
455  )
def _diagnostic
Print diagnostic information on spatial kernel and background fit.
A class to contain various attributes of the Psf.
Definition: PSF.h:66
def __init__
Create a ModelPsfMatchTask.
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
An integer coordinate rectangle.
Definition: Box.h:53
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:304
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.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.