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