LSSTApplications  18.1.0
LSSTDataManagementBasePackage
psfMatch.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 
22 __all__ = ["DetectionConfig", "PsfMatchConfig", "PsfMatchConfigAL", "PsfMatchConfigDF", "PsfMatchTask"]
23 
24 import time
25 
26 import numpy as np
27 
28 import lsst.afw.image as afwImage
29 import lsst.pex.config as pexConfig
30 import lsst.afw.math as afwMath
31 import lsst.afw.display as afwDisplay
32 import lsst.log as log
33 import lsst.pipe.base as pipeBase
34 from lsst.meas.algorithms import SubtractBackgroundConfig
35 from . import utils as diutils
36 from . import diffimLib
37 
38 
39 class DetectionConfig(pexConfig.Config):
40  """Configuration for detecting sources on images for building a
41  PSF-matching kernel
42 
43  Configuration for turning detected lsst.afw.detection.FootPrints into an
44  acceptable (unmasked, high signal-to-noise, not too large or not too small)
45  list of `lsst.ip.diffim.KernelSources` that are used to build the
46  Psf-matching kernel"""
47 
48  detThreshold = pexConfig.Field(
49  dtype=float,
50  doc="Value of footprint detection threshold",
51  default=10.0,
52  check=lambda x: x >= 3.0
53  )
54  detThresholdType = pexConfig.ChoiceField(
55  dtype=str,
56  doc="Type of detection threshold",
57  default="pixel_stdev",
58  allowed={
59  "value": "Use counts as the detection threshold type",
60  "stdev": "Use standard deviation of image plane",
61  "variance": "Use variance of image plane",
62  "pixel_stdev": "Use stdev derived from variance plane"
63  }
64  )
65  detOnTemplate = pexConfig.Field(
66  dtype=bool,
67  doc="""If true run detection on the template (image to convolve);
68  if false run detection on the science image""",
69  default=True
70  )
71  badMaskPlanes = pexConfig.ListField(
72  dtype=str,
73  doc="""Mask planes that lead to an invalid detection.
74  Options: NO_DATA EDGE SAT BAD CR INTRP""",
75  default=("NO_DATA", "EDGE", "SAT")
76  )
77  fpNpixMin = pexConfig.Field(
78  dtype=int,
79  doc="Minimum number of pixels in an acceptable Footprint",
80  default=5,
81  check=lambda x: x >= 5
82  )
83  fpNpixMax = pexConfig.Field(
84  dtype=int,
85  doc="""Maximum number of pixels in an acceptable Footprint;
86  too big and the subsequent convolutions become unwieldy""",
87  default=500,
88  check=lambda x: x <= 500
89  )
90  fpGrowKernelScaling = pexConfig.Field(
91  dtype=float,
92  doc="""If config.scaleByFwhm, grow the footprint based on
93  the final kernelSize. Each footprint will be
94  2*fpGrowKernelScaling*kernelSize x
95  2*fpGrowKernelScaling*kernelSize. With the value
96  of 1.0, the remaining pixels in each KernelCandiate
97  after convolution by the basis functions will be
98  equal to the kernel size itself.""",
99  default=1.0,
100  check=lambda x: x >= 1.0
101  )
102  fpGrowPix = pexConfig.Field(
103  dtype=int,
104  doc="""Growing radius (in pixels) for each raw detection
105  footprint. The smaller the faster; however the
106  kernel sum does not converge if the stamp is too
107  small; and the kernel is not constrained at all if
108  the stamp is the size of the kernel. The grown stamp
109  is 2 * fpGrowPix pixels larger in each dimension.
110  This is overridden by fpGrowKernelScaling if scaleByFwhm""",
111  default=30,
112  check=lambda x: x >= 10
113  )
114  scaleByFwhm = pexConfig.Field(
115  dtype=bool,
116  doc="Scale fpGrowPix by input Fwhm?",
117  default=True,
118  )
119 
120 
121 class PsfMatchConfig(pexConfig.Config):
122  """Base configuration for Psf-matching
123 
124  The base configuration of the Psf-matching kernel, and of the warping, detection,
125  and background modeling subTasks."""
126 
127  warpingConfig = pexConfig.ConfigField("Config for warping exposures to a common alignment",
128  afwMath.warper.WarperConfig)
129  detectionConfig = pexConfig.ConfigField("Controlling the detection of sources for kernel building",
130  DetectionConfig)
131  afwBackgroundConfig = pexConfig.ConfigField("Controlling the Afw background fitting",
132  SubtractBackgroundConfig)
133 
134  useAfwBackground = pexConfig.Field(
135  dtype=bool,
136  doc="Use afw background subtraction instead of ip_diffim",
137  default=False,
138  )
139  fitForBackground = pexConfig.Field(
140  dtype=bool,
141  doc="Include terms (including kernel cross terms) for background in ip_diffim",
142  default=False,
143  )
144  kernelBasisSet = pexConfig.ChoiceField(
145  dtype=str,
146  doc="Type of basis set for PSF matching kernel.",
147  default="alard-lupton",
148  allowed={
149  "alard-lupton": """Alard-Lupton sum-of-gaussians basis set,
150  * The first term has no spatial variation
151  * The kernel sum is conserved
152  * You may want to turn off 'usePcaForSpatialKernel'""",
153  "delta-function": """Delta-function kernel basis set,
154  * You may enable the option useRegularization
155  * You should seriously consider usePcaForSpatialKernel, which will also
156  enable kernel sum conservation for the delta function kernels"""
157  }
158  )
159  kernelSize = pexConfig.Field(
160  dtype=int,
161  doc="""Number of rows/columns in the convolution kernel; should be odd-valued.
162  Modified by kernelSizeFwhmScaling if scaleByFwhm = true""",
163  default=21,
164  )
165  scaleByFwhm = pexConfig.Field(
166  dtype=bool,
167  doc="Scale kernelSize, alardGaussians by input Fwhm",
168  default=True,
169  )
170  kernelSizeFwhmScaling = pexConfig.Field(
171  dtype=float,
172  doc="Multiplier of the largest AL Gaussian basis sigma to get the kernel bbox (pixel) size.",
173  default=6.0,
174  check=lambda x: x >= 1.0
175  )
176  kernelSizeMin = pexConfig.Field(
177  dtype=int,
178  doc="Minimum kernel bbox (pixel) size.",
179  default=21,
180  )
181  kernelSizeMax = pexConfig.Field(
182  dtype=int,
183  doc="Maximum kernel bbox (pixel) size.",
184  default=35,
185  )
186  spatialModelType = pexConfig.ChoiceField(
187  dtype=str,
188  doc="Type of spatial functions for kernel and background",
189  default="chebyshev1",
190  allowed={
191  "chebyshev1": "Chebyshev polynomial of the first kind",
192  "polynomial": "Standard x,y polynomial",
193  }
194  )
195  spatialKernelOrder = pexConfig.Field(
196  dtype=int,
197  doc="Spatial order of convolution kernel variation",
198  default=2,
199  check=lambda x: x >= 0
200  )
201  spatialBgOrder = pexConfig.Field(
202  dtype=int,
203  doc="Spatial order of differential background variation",
204  default=1,
205  check=lambda x: x >= 0
206  )
207  sizeCellX = pexConfig.Field(
208  dtype=int,
209  doc="Size (rows) in pixels of each SpatialCell for spatial modeling",
210  default=128,
211  check=lambda x: x >= 32
212  )
213  sizeCellY = pexConfig.Field(
214  dtype=int,
215  doc="Size (columns) in pixels of each SpatialCell for spatial modeling",
216  default=128,
217  check=lambda x: x >= 32
218  )
219  nStarPerCell = pexConfig.Field(
220  dtype=int,
221  doc="Number of KernelCandidates in each SpatialCell to use in the spatial fitting",
222  default=3,
223  check=lambda x: x >= 1
224  )
225  maxSpatialIterations = pexConfig.Field(
226  dtype=int,
227  doc="Maximum number of iterations for rejecting bad KernelCandidates in spatial fitting",
228  default=3,
229  check=lambda x: x >= 1 and x <= 5
230  )
231  usePcaForSpatialKernel = pexConfig.Field(
232  dtype=bool,
233  doc="""Use Pca to reduce the dimensionality of the kernel basis sets.
234  This is particularly useful for delta-function kernels.
235  Functionally, after all Cells have their raw kernels determined, we run
236  a Pca on these Kernels, re-fit the Cells using the eigenKernels and then
237  fit those for spatial variation using the same technique as for Alard-Lupton kernels.
238  If this option is used, the first term will have no spatial variation and the
239  kernel sum will be conserved.""",
240  default=False,
241  )
242  subtractMeanForPca = pexConfig.Field(
243  dtype=bool,
244  doc="Subtract off the mean feature before doing the Pca",
245  default=True,
246  )
247  numPrincipalComponents = pexConfig.Field(
248  dtype=int,
249  doc="""Number of principal components to use for Pca basis, including the
250  mean kernel if requested.""",
251  default=5,
252  check=lambda x: x >= 3
253  )
254  singleKernelClipping = pexConfig.Field(
255  dtype=bool,
256  doc="Do sigma clipping on each raw kernel candidate",
257  default=True,
258  )
259  kernelSumClipping = pexConfig.Field(
260  dtype=bool,
261  doc="Do sigma clipping on the ensemble of kernel sums",
262  default=True,
263  )
264  spatialKernelClipping = pexConfig.Field(
265  dtype=bool,
266  doc="Do sigma clipping after building the spatial model",
267  default=True,
268  )
269  checkConditionNumber = pexConfig.Field(
270  dtype=bool,
271  doc="""Test for maximum condition number when inverting a kernel matrix.
272  Anything above maxConditionNumber is not used and the candidate is set as BAD.
273  Also used to truncate inverse matrix in estimateBiasedRisk. However,
274  if you are doing any deconvolution you will want to turn this off, or use
275  a large maxConditionNumber""",
276  default=False,
277  )
278  badMaskPlanes = pexConfig.ListField(
279  dtype=str,
280  doc="""Mask planes to ignore when calculating diffim statistics
281  Options: NO_DATA EDGE SAT BAD CR INTRP""",
282  default=("NO_DATA", "EDGE", "SAT")
283  )
284  candidateResidualMeanMax = pexConfig.Field(
285  dtype=float,
286  doc="""Rejects KernelCandidates yielding bad difference image quality.
287  Used by BuildSingleKernelVisitor, AssessSpatialKernelVisitor.
288  Represents average over pixels of (image/sqrt(variance)).""",
289  default=0.25,
290  check=lambda x: x >= 0.0
291  )
292  candidateResidualStdMax = pexConfig.Field(
293  dtype=float,
294  doc="""Rejects KernelCandidates yielding bad difference image quality.
295  Used by BuildSingleKernelVisitor, AssessSpatialKernelVisitor.
296  Represents stddev over pixels of (image/sqrt(variance)).""",
297  default=1.50,
298  check=lambda x: x >= 0.0
299  )
300  useCoreStats = pexConfig.Field(
301  dtype=bool,
302  doc="""Use the core of the footprint for the quality statistics, instead of the entire footprint.
303  WARNING: if there is deconvolution we probably will need to turn this off""",
304  default=False,
305  )
306  candidateCoreRadius = pexConfig.Field(
307  dtype=int,
308  doc="""Radius for calculation of stats in 'core' of KernelCandidate diffim.
309  Total number of pixels used will be (2*radius)**2.
310  This is used both for 'core' diffim quality as well as ranking of
311  KernelCandidates by their total flux in this core""",
312  default=3,
313  check=lambda x: x >= 1
314  )
315  maxKsumSigma = pexConfig.Field(
316  dtype=float,
317  doc="""Maximum allowed sigma for outliers from kernel sum distribution.
318  Used to reject variable objects from the kernel model""",
319  default=3.0,
320  check=lambda x: x >= 0.0
321  )
322  maxConditionNumber = pexConfig.Field(
323  dtype=float,
324  doc="Maximum condition number for a well conditioned matrix",
325  default=5.0e7,
326  check=lambda x: x >= 0.0
327  )
328  conditionNumberType = pexConfig.ChoiceField(
329  dtype=str,
330  doc="Use singular values (SVD) or eigen values (EIGENVALUE) to determine condition number",
331  default="EIGENVALUE",
332  allowed={
333  "SVD": "Use singular values",
334  "EIGENVALUE": "Use eigen values (faster)",
335  }
336  )
337  maxSpatialConditionNumber = pexConfig.Field(
338  dtype=float,
339  doc="Maximum condition number for a well conditioned spatial matrix",
340  default=1.0e10,
341  check=lambda x: x >= 0.0
342  )
343  iterateSingleKernel = pexConfig.Field(
344  dtype=bool,
345  doc="""Remake KernelCandidate using better variance estimate after first pass?
346  Primarily useful when convolving a single-depth image, otherwise not necessary.""",
347  default=False,
348  )
349  constantVarianceWeighting = pexConfig.Field(
350  dtype=bool,
351  doc="""Use constant variance weighting in single kernel fitting?
352  In some cases this is better for bright star residuals.""",
353  default=True,
354  )
355  calculateKernelUncertainty = pexConfig.Field(
356  dtype=bool,
357  doc="""Calculate kernel and background uncertainties for each kernel candidate?
358  This comes from the inverse of the covariance matrix.
359  Warning: regularization can cause problems for this step.""",
360  default=False,
361  )
362  useBicForKernelBasis = pexConfig.Field(
363  dtype=bool,
364  doc="""Use Bayesian Information Criterion to select the number of bases going into the kernel""",
365  default=False,
366  )
367 
368 
370  """The parameters specific to the "Alard-Lupton" (sum-of-Gaussian) Psf-matching basis"""
371 
372  def setDefaults(self):
373  PsfMatchConfig.setDefaults(self)
374  self.kernelBasisSet = "alard-lupton"
375  self.maxConditionNumber = 5.0e7
376 
377  alardNGauss = pexConfig.Field(
378  dtype=int,
379  doc="Number of Gaussians in alard-lupton basis",
380  default=3,
381  check=lambda x: x >= 1
382  )
383  alardDegGauss = pexConfig.ListField(
384  dtype=int,
385  doc="Polynomial order of spatial modification of Gaussian basis functions. "
386  "List length must be `alardNGauss`.",
387  default=(4, 2, 2),
388  )
389  alardSigGauss = pexConfig.ListField(
390  dtype=float,
391  doc="Default sigma values in pixels of Gaussian kernel base functions. "
392  "List length must be `alardNGauss`.",
393  default=(0.7, 1.5, 3.0),
394  )
395  alardGaussBeta = pexConfig.Field(
396  dtype=float,
397  doc="""Multiplier of Gaussian sigmas between adjacent elements of the kernel basis list.""",
398  default=2.0,
399  check=lambda x: x >= 0.0,
400  )
401  alardMinSig = pexConfig.Field(
402  dtype=float,
403  doc="""Minimum Sigma (pixels) for Gaussians""",
404  default=0.7,
405  check=lambda x: x >= 0.25
406  )
407  alardDegGaussDeconv = pexConfig.Field(
408  dtype=int,
409  doc="""Degree of spatial modification of ALL gaussians in AL basis during deconvolution""",
410  default=3,
411  check=lambda x: x >= 1
412  )
413  alardMinSigDeconv = pexConfig.Field(
414  dtype=float,
415  doc="Minimum Sigma (pixels) for Gaussians during deconvolution; "
416  "make smaller than alardMinSig as this is only indirectly used",
417  default=0.4,
418  check=lambda x: x >= 0.25
419  )
420  alardNGaussDeconv = pexConfig.Field(
421  dtype=int,
422  doc="Number of Gaussians in AL basis during deconvolution",
423  default=3,
424  check=lambda x: x >= 1
425  )
426 
427 
429  """The parameters specific to the delta-function (one basis per-pixel) Psf-matching basis"""
430 
431  def setDefaults(self):
432  PsfMatchConfig.setDefaults(self)
433  self.kernelBasisSet = "delta-function"
434  self.maxConditionNumber = 5.0e6
436  self.subtractMeanForPca = True
437  self.useBicForKernelBasis = False
438 
439  useRegularization = pexConfig.Field(
440  dtype=bool,
441  doc="Use regularization to smooth the delta function kernels",
442  default=True,
443  )
444  regularizationType = pexConfig.ChoiceField(
445  dtype=str,
446  doc="Type of regularization.",
447  default="centralDifference",
448  allowed={
449  "centralDifference": "Penalize second derivative using 2-D stencil of central finite difference",
450  "forwardDifference": "Penalize first, second, third derivatives using forward finite differeces"
451  }
452  )
453  centralRegularizationStencil = pexConfig.ChoiceField(
454  dtype=int,
455  doc="Type of stencil to approximate central derivative (for centralDifference only)",
456  default=9,
457  allowed={
458  5: "5-point stencil including only adjacent-in-x,y elements",
459  9: "9-point stencil including diagonal elements"
460  }
461  )
462  forwardRegularizationOrders = pexConfig.ListField(
463  dtype=int,
464  doc="Array showing which order derivatives to penalize (for forwardDifference only)",
465  default=(1, 2),
466  itemCheck=lambda x: (x > 0) and (x < 4)
467  )
468  regularizationBorderPenalty = pexConfig.Field(
469  dtype=float,
470  doc="Value of the penalty for kernel border pixels",
471  default=3.0,
472  check=lambda x: x >= 0.0
473  )
474  lambdaType = pexConfig.ChoiceField(
475  dtype=str,
476  doc="How to choose the value of the regularization strength",
477  default="absolute",
478  allowed={
479  "absolute": "Use lambdaValue as the value of regularization strength",
480  "relative": "Use lambdaValue as fraction of the default regularization strength (N.R. 18.5.8)",
481  "minimizeBiasedRisk": "Minimize biased risk estimate",
482  "minimizeUnbiasedRisk": "Minimize unbiased risk estimate",
483  }
484  )
485  lambdaValue = pexConfig.Field(
486  dtype=float,
487  doc="Value used for absolute determinations of regularization strength",
488  default=0.2,
489  )
490  lambdaScaling = pexConfig.Field(
491  dtype=float,
492  doc="Fraction of the default lambda strength (N.R. 18.5.8) to use. 1e-4 or 1e-5",
493  default=1e-4,
494  )
495  lambdaStepType = pexConfig.ChoiceField(
496  dtype=str,
497  doc="""If a scan through lambda is needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
498  use log or linear steps""",
499  default="log",
500  allowed={
501  "log": "Step in log intervals; e.g. lambdaMin, lambdaMax, lambdaStep = -1.0, 2.0, 0.1",
502  "linear": "Step in linear intervals; e.g. lambdaMin, lambdaMax, lambdaStep = 0.1, 100, 0.1",
503  }
504  )
505  lambdaMin = pexConfig.Field(
506  dtype=float,
507  doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
508  start at this value. If lambdaStepType = log:linear, suggest -1:0.1""",
509  default=-1.0,
510  )
511  lambdaMax = pexConfig.Field(
512  dtype=float,
513  doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
514  stop at this value. If lambdaStepType = log:linear, suggest 2:100""",
515  default=2.0,
516  )
517  lambdaStep = pexConfig.Field(
518  dtype=float,
519  doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
520  step in these increments. If lambdaStepType = log:linear, suggest 0.1:0.1""",
521  default=0.1,
522  )
523 
524 
525 class PsfMatchTask(pipeBase.Task):
526  """Base class for Psf Matching; should not be called directly
527 
528  Notes
529  -----
530  PsfMatchTask is a base class that implements the core functionality for matching the
531  Psfs of two images using a spatially varying Psf-matching `lsst.afw.math.LinearCombinationKernel`.
532  The Task requires the user to provide an instance of an `lsst.afw.math.SpatialCellSet`,
533  filled with `lsst.ip.diffim.KernelCandidate` instances, and a list of `lsst.afw.math.Kernels`
534  of basis shapes that will be used for the decomposition. If requested, the Task
535  also performs background matching and returns the differential background model as an
536  `lsst.afw.math.Kernel.SpatialFunction`.
537 
538  Invoking the Task
539  -----------------
540 
541  As a base class, this Task is not directly invoked. However, ``run()`` methods that are
542  implemented on derived classes will make use of the core ``_solve()`` functionality,
543  which defines a sequence of `lsst.afw.math.CandidateVisitor` classes that iterate
544  through the KernelCandidates, first building up a per-candidate solution and then
545  building up a spatial model from the ensemble of candidates. Sigma clipping is
546  performed using the mean and standard deviation of all kernel sums (to reject
547  variable objects), on the per-candidate substamp diffim residuals
548  (to indicate a bad choice of kernel basis shapes for that particular object),
549  and on the substamp diffim residuals using the spatial kernel fit (to indicate a bad
550  choice of spatial kernel order, or poor constraints on the spatial model). The
551  ``_diagnostic()`` method logs information on the quality of the spatial fit, and also
552  modifies the Task metadata.
553 
554  .. list-table:: Quantities set in Metadata
555  :header-rows: 1
556 
557  * - Parameter
558  - Description
559  * - ``spatialConditionNum``
560  - Condition number of the spatial kernel fit
561  * - ``spatialKernelSum``
562  - Kernel sum (10^{-0.4 * ``Delta``; zeropoint}) of the spatial Psf-matching kernel
563  * - ``ALBasisNGauss``
564  - If using sum-of-Gaussian basis, the number of gaussians used
565  * - ``ALBasisDegGauss``
566  - If using sum-of-Gaussian basis, the deg of spatial variation of the Gaussians
567  * - ``ALBasisSigGauss``
568  - If using sum-of-Gaussian basis, the widths (sigma) of the Gaussians
569  * - ``ALKernelSize``
570  - If using sum-of-Gaussian basis, the kernel size
571  * - ``NFalsePositivesTotal``
572  - Total number of diaSources
573  * - ``NFalsePositivesRefAssociated``
574  - Number of diaSources that associate with the reference catalog
575  * - ``NFalsePositivesRefAssociated``
576  - Number of diaSources that associate with the source catalog
577  * - ``NFalsePositivesUnassociated``
578  - Number of diaSources that are orphans
579  * - ``metric_MEAN``
580  - Mean value of substamp diffim quality metrics across all KernelCandidates,
581  for both the per-candidate (LOCAL) and SPATIAL residuals
582  * - ``metric_MEDIAN``
583  - Median value of substamp diffim quality metrics across all KernelCandidates,
584  for both the per-candidate (LOCAL) and SPATIAL residuals
585  * - ``metric_STDEV``
586  - Standard deviation of substamp diffim quality metrics across all KernelCandidates,
587  for both the per-candidate (LOCAL) and SPATIAL residuals
588 
589  Debug variables
590  ---------------
591 
592  The `lsst.pipe.base.cmdLineTask.CmdLineTask` command line task interface supports a
593  flag -d/--debug to import @b debug.py from your PYTHONPATH. The relevant contents of debug.py
594  for this Task include:
595 
596  .. code-block:: py
597 
598  import sys
599  import lsstDebug
600  def DebugInfo(name):
601  di = lsstDebug.getInfo(name)
602  if name == "lsst.ip.diffim.psfMatch":
603  # enable debug output
604  di.display = True
605  # display mask transparency
606  di.maskTransparency = 80
607  # show all the candidates and residuals
608  di.displayCandidates = True
609  # show kernel basis functions
610  di.displayKernelBasis = False
611  # show kernel realized across the image
612  di.displayKernelMosaic = True
613  # show coefficients of spatial model
614  di.plotKernelSpatialModel = False
615  # show the bad candidates (red) along with good (green)
616  di.showBadCandidates = True
617  return di
618  lsstDebug.Info = DebugInfo
619  lsstDebug.frame = 1
620 
621  Note that if you want additional logging info, you may add to your scripts:
622 
623  .. code-block:: py
624 
625  import lsst.log.utils as logUtils
626  logUtils.traceSetAt("ip.diffim", 4)
627  """
628  ConfigClass = PsfMatchConfig
629  _DefaultName = "psfMatch"
630 
631  def __init__(self, *args, **kwargs):
632  """Create the psf-matching Task
633 
634  Parameters
635  ----------
636  *args
637  Arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
638  **kwargs
639  Keyword arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
640 
641  Notes
642  -----
643  The initialization sets the Psf-matching kernel configuration using the value of
644  self.config.kernel.active. If the kernel is requested with regularization to moderate
645  the bias/variance tradeoff, currently only used when a delta function kernel basis
646  is provided, it creates a regularization matrix stored as member variable
647  self.hMat.
648  """
649  pipeBase.Task.__init__(self, *args, **kwargs)
650  self.kConfig = self.config.kernel.active
651 
652  if 'useRegularization' in self.kConfig:
653  self.useRegularization = self.kConfig.useRegularization
654  else:
655  self.useRegularization = False
656 
657  if self.useRegularization:
658  self.hMat = diffimLib.makeRegularizationMatrix(pexConfig.makePolicy(self.kConfig))
659 
660  def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
661  """Provide logging diagnostics on quality of spatial kernel fit
662 
663  Parameters
664  ----------
665  kernelCellSet : `lsst.afw.math.SpatialCellSet`
666  Cellset that contains the KernelCandidates used in the fitting
667  spatialSolution : `lsst.ip.diffim.SpatialKernelSolution`
668  KernelSolution of best-fit
669  spatialKernel : `lsst.afw.math.LinearCombinationKernel`
670  Best-fit spatial Kernel model
671  spatialBg : `lsst.afw.math.Function2D`
672  Best-fit spatial background model
673  """
674  # What is the final kernel sum
675  kImage = afwImage.ImageD(spatialKernel.getDimensions())
676  kSum = spatialKernel.computeImage(kImage, False)
677  self.log.info("Final spatial kernel sum %.3f" % (kSum))
678 
679  # Look at how well conditioned the matrix is
680  conditionNum = spatialSolution.getConditionNumber(
681  getattr(diffimLib.KernelSolution, self.kConfig.conditionNumberType))
682  self.log.info("Spatial model condition number %.3e" % (conditionNum))
683 
684  if conditionNum < 0.0:
685  self.log.warn("Condition number is negative (%.3e)" % (conditionNum))
686  if conditionNum > self.kConfig.maxSpatialConditionNumber:
687  self.log.warn("Spatial solution exceeds max condition number (%.3e > %.3e)" % (
688  conditionNum, self.kConfig.maxSpatialConditionNumber))
689 
690  self.metadata.set("spatialConditionNum", conditionNum)
691  self.metadata.set("spatialKernelSum", kSum)
692 
693  # Look at how well the solution is constrained
694  nBasisKernels = spatialKernel.getNBasisKernels()
695  nKernelTerms = spatialKernel.getNSpatialParameters()
696  if nKernelTerms == 0: # order 0
697  nKernelTerms = 1
698 
699  # Not fit for
700  nBgTerms = spatialBg.getNParameters()
701  if nBgTerms == 1:
702  if spatialBg.getParameters()[0] == 0.0:
703  nBgTerms = 0
704 
705  nGood = 0
706  nBad = 0
707  nTot = 0
708  for cell in kernelCellSet.getCellList():
709  for cand in cell.begin(False): # False = include bad candidates
710  nTot += 1
711  if cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
712  nGood += 1
713  if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
714  nBad += 1
715 
716  self.log.info("Doing stats of kernel candidates used in the spatial fit.")
717 
718  # Counting statistics
719  if nBad > 2*nGood:
720  self.log.warn("Many more candidates rejected than accepted; %d total, %d rejected, %d used" % (
721  nTot, nBad, nGood))
722  else:
723  self.log.info("%d candidates total, %d rejected, %d used" % (nTot, nBad, nGood))
724 
725  # Some judgements on the quality of the spatial models
726  if nGood < nKernelTerms:
727  self.log.warn("Spatial kernel model underconstrained; %d candidates, %d terms, %d bases" % (
728  nGood, nKernelTerms, nBasisKernels))
729  self.log.warn("Consider lowering the spatial order")
730  elif nGood <= 2*nKernelTerms:
731  self.log.warn("Spatial kernel model poorly constrained; %d candidates, %d terms, %d bases" % (
732  nGood, nKernelTerms, nBasisKernels))
733  self.log.warn("Consider lowering the spatial order")
734  else:
735  self.log.info("Spatial kernel model well constrained; %d candidates, %d terms, %d bases" % (
736  nGood, nKernelTerms, nBasisKernels))
737 
738  if nGood < nBgTerms:
739  self.log.warn("Spatial background model underconstrained; %d candidates, %d terms" % (
740  nGood, nBgTerms))
741  self.log.warn("Consider lowering the spatial order")
742  elif nGood <= 2*nBgTerms:
743  self.log.warn("Spatial background model poorly constrained; %d candidates, %d terms" % (
744  nGood, nBgTerms))
745  self.log.warn("Consider lowering the spatial order")
746  else:
747  self.log.info("Spatial background model appears well constrained; %d candidates, %d terms" % (
748  nGood, nBgTerms))
749 
750  def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground):
751  """Provide visualization of the inputs and ouputs to the Psf-matching code
752 
753  Parameters
754  ----------
755  kernelCellSet : `lsst.afw.math.SpatialCellSet`
756  The SpatialCellSet used in determining the matching kernel and background
757  spatialKernel : `lsst.afw.math.LinearCombinationKernel`
758  Spatially varying Psf-matching kernel
759  spatialBackground : `lsst.afw.math.Function2D`
760  Spatially varying background-matching function
761  """
762  import lsstDebug
763  displayCandidates = lsstDebug.Info(__name__).displayCandidates
764  displayKernelBasis = lsstDebug.Info(__name__).displayKernelBasis
765  displayKernelMosaic = lsstDebug.Info(__name__).displayKernelMosaic
766  plotKernelSpatialModel = lsstDebug.Info(__name__).plotKernelSpatialModel
767  showBadCandidates = lsstDebug.Info(__name__).showBadCandidates
768  maskTransparency = lsstDebug.Info(__name__).maskTransparency
769  if not maskTransparency:
770  maskTransparency = 0
771  afwDisplay.setDefaultMaskTransparency(maskTransparency)
772 
773  if displayCandidates:
774  diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
775  frame=lsstDebug.frame,
776  showBadCandidates=showBadCandidates)
777  lsstDebug.frame += 1
778  diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
779  frame=lsstDebug.frame,
780  showBadCandidates=showBadCandidates,
781  kernels=True)
782  lsstDebug.frame += 1
783  diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
784  frame=lsstDebug.frame,
785  showBadCandidates=showBadCandidates,
786  resids=True)
787  lsstDebug.frame += 1
788 
789  if displayKernelBasis:
790  diutils.showKernelBasis(spatialKernel, frame=lsstDebug.frame)
791  lsstDebug.frame += 1
792 
793  if displayKernelMosaic:
794  diutils.showKernelMosaic(kernelCellSet.getBBox(), spatialKernel, frame=lsstDebug.frame)
795  lsstDebug.frame += 1
796 
797  if plotKernelSpatialModel:
798  diutils.plotKernelSpatialModel(spatialKernel, kernelCellSet, showBadCandidates=showBadCandidates)
799 
800  def _createPcaBasis(self, kernelCellSet, nStarPerCell, policy):
801  """Create Principal Component basis
802 
803  If a principal component analysis is requested, typically when using a delta function basis,
804  perform the PCA here and return a new basis list containing the new principal components.
805 
806  Parameters
807  ----------
808  kernelCellSet : `lsst.afw.math.SpatialCellSet`
809  a SpatialCellSet containing KernelCandidates, from which components are derived
810  nStarPerCell : `int`
811  the number of stars per cell to visit when doing the PCA
812  policy : `lsst.pex.policy.Policy`
813  input policy controlling the single kernel visitor
814 
815  Returns
816  -------
817  nRejectedPca : `int`
818  number of KernelCandidates rejected during PCA loop
819  spatialBasisList : `list` of `lsst.afw.math.kernel.FixedKernel`
820  basis list containing the principal shapes as Kernels
821 
822  Raises
823  ------
824  RuntimeError
825  If the Eigenvalues sum to zero.
826  """
827  nComponents = self.kConfig.numPrincipalComponents
828  imagePca = diffimLib.KernelPcaD()
829  importStarVisitor = diffimLib.KernelPcaVisitorF(imagePca)
830  kernelCellSet.visitCandidates(importStarVisitor, nStarPerCell)
831  if self.kConfig.subtractMeanForPca:
832  importStarVisitor.subtractMean()
833  imagePca.analyze()
834 
835  eigenValues = imagePca.getEigenValues()
836  pcaBasisList = importStarVisitor.getEigenKernels()
837 
838  eSum = np.sum(eigenValues)
839  if eSum == 0.0:
840  raise RuntimeError("Eigenvalues sum to zero")
841  for j in range(len(eigenValues)):
842  log.log("TRACE5." + self.log.getName() + "._solve", log.DEBUG,
843  "Eigenvalue %d : %f (%f)", j, eigenValues[j], eigenValues[j]/eSum)
844 
845  nToUse = min(nComponents, len(eigenValues))
846  trimBasisList = []
847  for j in range(nToUse):
848  # Check for NaNs?
849  kimage = afwImage.ImageD(pcaBasisList[j].getDimensions())
850  pcaBasisList[j].computeImage(kimage, False)
851  if not (True in np.isnan(kimage.getArray())):
852  trimBasisList.append(pcaBasisList[j])
853 
854  # Put all the power in the first kernel, which will not vary spatially
855  spatialBasisList = diffimLib.renormalizeKernelList(trimBasisList)
856 
857  # New Kernel visitor for this new basis list (no regularization explicitly)
858  singlekvPca = diffimLib.BuildSingleKernelVisitorF(spatialBasisList, policy)
859  singlekvPca.setSkipBuilt(False)
860  kernelCellSet.visitCandidates(singlekvPca, nStarPerCell)
861  singlekvPca.setSkipBuilt(True)
862  nRejectedPca = singlekvPca.getNRejected()
863 
864  return nRejectedPca, spatialBasisList
865 
866  def _buildCellSet(self, *args):
867  """Fill a SpatialCellSet with KernelCandidates for the Psf-matching process;
868  override in derived classes"""
869  return
870 
871  @pipeBase.timeMethod
872  def _solve(self, kernelCellSet, basisList, returnOnExcept=False):
873  """Solve for the PSF matching kernel
874 
875  Parameters
876  ----------
877  kernelCellSet : `lsst.afw.math.SpatialCellSet`
878  a SpatialCellSet to use in determining the matching kernel
879  (typically as provided by _buildCellSet)
880  basisList : `list` of `lsst.afw.math.kernel.FixedKernel`
881  list of Kernels to be used in the decomposition of the spatially varying kernel
882  (typically as provided by makeKernelBasisList)
883  returnOnExcept : `bool`, optional
884  if True then return (None, None) if an error occurs, else raise the exception
885 
886  Returns
887  -------
888  psfMatchingKernel : `lsst.afw.math.LinearCombinationKernel`
889  Spatially varying Psf-matching kernel
890  backgroundModel : `lsst.afw.math.Function2D`
891  Spatially varying background-matching function
892 
893  Raises
894  ------
895  RuntimeError :
896  If unable to determine PSF matching kernel and ``returnOnExcept==False``.
897  """
898 
899  import lsstDebug
900  display = lsstDebug.Info(__name__).display
901 
902  maxSpatialIterations = self.kConfig.maxSpatialIterations
903  nStarPerCell = self.kConfig.nStarPerCell
904  usePcaForSpatialKernel = self.kConfig.usePcaForSpatialKernel
905 
906  # Visitor for the single kernel fit
907  policy = pexConfig.makePolicy(self.kConfig)
908  if self.useRegularization:
909  singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, policy, self.hMat)
910  else:
911  singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, policy)
912 
913  # Visitor for the kernel sum rejection
914  ksv = diffimLib.KernelSumVisitorF(policy)
915 
916  # Main loop
917  t0 = time.time()
918  try:
919  totalIterations = 0
920  thisIteration = 0
921  while (thisIteration < maxSpatialIterations):
922 
923  # Make sure there are no uninitialized candidates as active occupants of Cell
924  nRejectedSkf = -1
925  while (nRejectedSkf != 0):
926  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
927  "Building single kernels...")
928  kernelCellSet.visitCandidates(singlekv, nStarPerCell)
929  nRejectedSkf = singlekv.getNRejected()
930  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
931  "Iteration %d, rejected %d candidates due to initial kernel fit",
932  thisIteration, nRejectedSkf)
933 
934  # Reject outliers in kernel sum
935  ksv.resetKernelSum()
936  ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE)
937  kernelCellSet.visitCandidates(ksv, nStarPerCell)
938  ksv.processKsumDistribution()
939  ksv.setMode(diffimLib.KernelSumVisitorF.REJECT)
940  kernelCellSet.visitCandidates(ksv, nStarPerCell)
941 
942  nRejectedKsum = ksv.getNRejected()
943  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
944  "Iteration %d, rejected %d candidates due to kernel sum",
945  thisIteration, nRejectedKsum)
946 
947  # Do we jump back to the top without incrementing thisIteration?
948  if nRejectedKsum > 0:
949  totalIterations += 1
950  continue
951 
952  # At this stage we can either apply the spatial fit to
953  # the kernels, or we run a PCA, use these as a *new*
954  # basis set with lower dimensionality, and then apply
955  # the spatial fit to these kernels
956 
957  if (usePcaForSpatialKernel):
958  log.log("TRACE0." + self.log.getName() + "._solve", log.DEBUG,
959  "Building Pca basis")
960 
961  nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, policy)
962  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
963  "Iteration %d, rejected %d candidates due to Pca kernel fit",
964  thisIteration, nRejectedPca)
965 
966  # We don't want to continue on (yet) with the
967  # spatial modeling, because we have bad objects
968  # contributing to the Pca basis. We basically
969  # need to restart from the beginning of this loop,
970  # since the cell-mates of those objects that were
971  # rejected need their original Kernels built by
972  # singleKernelFitter.
973 
974  # Don't count against thisIteration
975  if (nRejectedPca > 0):
976  totalIterations += 1
977  continue
978  else:
979  spatialBasisList = basisList
980 
981  # We have gotten on to the spatial modeling part
982  regionBBox = kernelCellSet.getBBox()
983  spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, policy)
984  kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
985  spatialkv.solveLinearEquation()
986  log.log("TRACE2." + self.log.getName() + "._solve", log.DEBUG,
987  "Spatial kernel built with %d candidates", spatialkv.getNCandidates())
988  spatialKernel, spatialBackground = spatialkv.getSolutionPair()
989 
990  # Check the quality of the spatial fit (look at residuals)
991  assesskv = diffimLib.AssessSpatialKernelVisitorF(spatialKernel, spatialBackground, policy)
992  kernelCellSet.visitCandidates(assesskv, nStarPerCell)
993  nRejectedSpatial = assesskv.getNRejected()
994  nGoodSpatial = assesskv.getNGood()
995  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
996  "Iteration %d, rejected %d candidates due to spatial kernel fit",
997  thisIteration, nRejectedSpatial)
998  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG,
999  "%d candidates used in fit", nGoodSpatial)
1000 
1001  # If only nGoodSpatial == 0, might be other candidates in the cells
1002  if nGoodSpatial == 0 and nRejectedSpatial == 0:
1003  raise RuntimeError("No kernel candidates for spatial fit")
1004 
1005  if nRejectedSpatial == 0:
1006  # Nothing rejected, finished with spatial fit
1007  break
1008 
1009  # Otherwise, iterate on...
1010  thisIteration += 1
1011 
1012  # Final fit if above did not converge
1013  if (nRejectedSpatial > 0) and (thisIteration == maxSpatialIterations):
1014  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG, "Final spatial fit")
1015  if (usePcaForSpatialKernel):
1016  nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, policy)
1017  regionBBox = kernelCellSet.getBBox()
1018  spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, policy)
1019  kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
1020  spatialkv.solveLinearEquation()
1021  log.log("TRACE2." + self.log.getName() + "._solve", log.DEBUG,
1022  "Spatial kernel built with %d candidates", spatialkv.getNCandidates())
1023  spatialKernel, spatialBackground = spatialkv.getSolutionPair()
1024 
1025  spatialSolution = spatialkv.getKernelSolution()
1026 
1027  except Exception as e:
1028  self.log.error("ERROR: Unable to calculate psf matching kernel")
1029 
1030  log.log("TRACE1." + self.log.getName() + "._solve", log.DEBUG, str(e))
1031  raise e
1032 
1033  t1 = time.time()
1034  log.log("TRACE0." + self.log.getName() + "._solve", log.DEBUG,
1035  "Total time to compute the spatial kernel : %.2f s", (t1 - t0))
1036 
1037  if display:
1038  self._displayDebug(kernelCellSet, spatialKernel, spatialBackground)
1039 
1040  self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground)
1041 
1042  return spatialSolution, spatialKernel, spatialBackground
1043 
1044 
1045 PsfMatch = PsfMatchTask
def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg)
Definition: psfMatch.py:660
def __init__(self, args, kwargs)
Definition: psfMatch.py:631
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
daf::base::PropertySet * set
Definition: fits.cc:884
int min
Definition: Log.h:691
def _createPcaBasis(self, kernelCellSet, nStarPerCell, policy)
Definition: psfMatch.py:800
def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground)
Definition: psfMatch.py:750
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...