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