LSST Applications g013ef56533+d2224463a4,g199a45376c+0ba108daf9,g19c4beb06c+9f335b2115,g1fd858c14a+2459ca3e43,g210f2d0738+2d3d333a78,g262e1987ae+abbb004f04,g2825c19fe3+eedc38578d,g29ae962dfc+0cb55f06ef,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+19c3a54948,g47891489e3+501a489530,g4cdb532a89+a047e97985,g511e8cfd20+ce1f47b6d6,g53246c7159+8c5ae1fdc5,g54cd7ddccb+890c8e1e5d,g5fd55ab2c7+951cc3f256,g64539dfbff+2d3d333a78,g67b6fd64d1+501a489530,g67fd3c3899+2d3d333a78,g74acd417e5+0ea5dee12c,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+501a489530,g8d7436a09f+5ea4c44d25,g8ea07a8fe4+81eaaadc04,g90f42f885a+34c0557caf,g9486f8a5af+165c016931,g97be763408+d5e351dcc8,gbf99507273+8c5ae1fdc5,gc2a301910b+2d3d333a78,gca7fc764a6+501a489530,gce8aa8abaa+8c5ae1fdc5,gd7ef33dd92+501a489530,gdab6d2f7ff+0ea5dee12c,ge410e46f29+501a489530,geaed405ab2+e3b4b2a692,gf9a733ac38+8c5ae1fdc5,w.2025.41
LSST Data Management Base Package
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",
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 kSum = spatialKernel.computeImage(kImage, False)
599 self.log.info("Final spatial kernel sum %.3f", kSum)
600
601 # Look at how well conditioned the matrix is
602 conditionNum = spatialSolution.getConditionNumber(
603 getattr(diffimLib.KernelSolution, self.kConfig.conditionNumberType))
604 self.log.info("Spatial model condition number %.3e", conditionNum)
605
606 if conditionNum < 0.0:
607 self.log.warning("Condition number is negative (%.3e)", conditionNum)
608 if conditionNum > self.kConfig.maxSpatialConditionNumber:
609 self.log.warning("Spatial solution exceeds max condition number (%.3e > %.3e)",
610 conditionNum, self.kConfig.maxSpatialConditionNumber)
611
612 self.metadata["spatialConditionNum"] = conditionNum
613 self.metadata["spatialKernelSum"] = kSum
614
615 # Look at how well the solution is constrained
616 nBasisKernels = spatialKernel.getNBasisKernels()
617 nKernelTerms = spatialKernel.getNSpatialParameters()
618 if nKernelTerms == 0: # order 0
619 nKernelTerms = 1
620
621 # Not fit for
622 nBgTerms = spatialBg.getNParameters()
623 if nBgTerms == 1:
624 if spatialBg.getParameters()[0] == 0.0:
625 nBgTerms = 0
626
627 nGood = 0
628 nBad = 0
629 nTot = 0
630 for cell in kernelCellSet.getCellList():
631 for cand in cell.begin(False): # False = include bad candidates
632 nTot += 1
633 if cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
634 nGood += 1
635 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
636 nBad += 1
637
638 self.log.info("Doing stats of kernel candidates used in the spatial fit.")
639
640 # Counting statistics
641 if nBad > 2*nGood:
642 self.log.warning("Many more candidates rejected than accepted; %d total, %d rejected, %d used",
643 nTot, nBad, nGood)
644 else:
645 self.log.info("%d candidates total, %d rejected, %d used", nTot, nBad, nGood)
646
647 # Some judgements on the quality of the spatial models
648 if nGood < nKernelTerms:
649 self.log.warning("Spatial kernel model underconstrained; %d candidates, %d terms, %d bases",
650 nGood, nKernelTerms, nBasisKernels)
651 self.log.warning("Consider lowering the spatial order")
652 elif nGood <= 2*nKernelTerms:
653 self.log.warning("Spatial kernel model poorly constrained; %d candidates, %d terms, %d bases",
654 nGood, nKernelTerms, nBasisKernels)
655 self.log.warning("Consider lowering the spatial order")
656 else:
657 self.log.info("Spatial kernel model well constrained; %d candidates, %d terms, %d bases",
658 nGood, nKernelTerms, nBasisKernels)
659
660 if nGood < nBgTerms:
661 self.log.warning("Spatial background model underconstrained; %d candidates, %d terms",
662 nGood, nBgTerms)
663 self.log.warning("Consider lowering the spatial order")
664 elif nGood <= 2*nBgTerms:
665 self.log.warning("Spatial background model poorly constrained; %d candidates, %d terms",
666 nGood, nBgTerms)
667 self.log.warning("Consider lowering the spatial order")
668 else:
669 self.log.info("Spatial background model appears well constrained; %d candidates, %d terms",
670 nGood, nBgTerms)
671
672 def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground):
673 """Provide visualization of the inputs and ouputs to the Psf-matching code
674
675 Parameters
676 ----------
677 kernelCellSet : `lsst.afw.math.SpatialCellSet`
678 The SpatialCellSet used in determining the matching kernel and background
679 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
680 Spatially varying Psf-matching kernel
681 spatialBackground : `lsst.afw.math.Function2D`
682 Spatially varying background-matching function
683 """
684 import lsstDebug
685 displayCandidates = lsstDebug.Info(__name__).displayCandidates
686 displayKernelBasis = lsstDebug.Info(__name__).displayKernelBasis
687 displayKernelMosaic = lsstDebug.Info(__name__).displayKernelMosaic
688 plotKernelSpatialModel = lsstDebug.Info(__name__).plotKernelSpatialModel
689 plotKernelCoefficients = lsstDebug.Info(__name__).plotKernelCoefficients
690 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates
691 maskTransparency = lsstDebug.Info(__name__).maskTransparency
692 if not maskTransparency:
693 maskTransparency = 0
694 afwDisplay.setDefaultMaskTransparency(maskTransparency)
695
696 if displayCandidates:
697 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
698 frame=lsstDebug.frame,
699 showBadCandidates=showBadCandidates)
700 lsstDebug.frame += 1
701 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
702 frame=lsstDebug.frame,
703 showBadCandidates=showBadCandidates,
704 kernels=True)
705 lsstDebug.frame += 1
706 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
707 frame=lsstDebug.frame,
708 showBadCandidates=showBadCandidates,
709 resids=True)
710 lsstDebug.frame += 1
711
712 if displayKernelBasis:
713 diutils.showKernelBasis(spatialKernel, frame=lsstDebug.frame)
714 lsstDebug.frame += 1
715
716 if displayKernelMosaic:
717 diutils.showKernelMosaic(kernelCellSet.getBBox(), spatialKernel, frame=lsstDebug.frame)
718 lsstDebug.frame += 1
719
720 if plotKernelSpatialModel:
721 diutils.plotKernelSpatialModel(spatialKernel, kernelCellSet, showBadCandidates=showBadCandidates)
722
723 if plotKernelCoefficients:
724 diutils.plotKernelCoefficients(spatialKernel, kernelCellSet)
725
726 def _createPcaBasis(self, kernelCellSet, nStarPerCell, ps):
727 """Create Principal Component basis
728
729 If a principal component analysis is requested, typically when using a delta function basis,
730 perform the PCA here and return a new basis list containing the new principal components.
731
732 Parameters
733 ----------
734 kernelCellSet : `lsst.afw.math.SpatialCellSet`
735 a SpatialCellSet containing KernelCandidates, from which components are derived
736 nStarPerCell : `int`
737 the number of stars per cell to visit when doing the PCA
738 ps : `lsst.daf.base.PropertySet`
739 input property set controlling the single kernel visitor
740
741 Returns
742 -------
743 nRejectedPca : `int`
744 number of KernelCandidates rejected during PCA loop
745 spatialBasisList : `list` of `lsst.afw.math.kernel.FixedKernel`
746 basis list containing the principal shapes as Kernels
747
748 Raises
749 ------
750 RuntimeError
751 If the Eigenvalues sum to zero.
752 """
753 nComponents = self.kConfig.numPrincipalComponents
754 imagePca = diffimLib.KernelPcaD()
755 importStarVisitor = diffimLib.KernelPcaVisitorF(imagePca)
756 kernelCellSet.visitCandidates(importStarVisitor, nStarPerCell)
757 if self.kConfig.subtractMeanForPca:
758 importStarVisitor.subtractMean()
759 imagePca.analyze()
760
761 eigenValues = imagePca.getEigenValues()
762 pcaBasisList = importStarVisitor.getEigenKernels()
763
764 eSum = np.sum(eigenValues)
765 if eSum == 0.0:
766 raise RuntimeError("Eigenvalues sum to zero")
767 trace_logger = getTraceLogger(self.log.getChild("_solve"), 5)
768 for j in range(len(eigenValues)):
769 trace_logger.debug("Eigenvalue %d : %f (%f)", j, eigenValues[j], eigenValues[j]/eSum)
770
771 nToUse = min(nComponents, len(eigenValues))
772 trimBasisList = []
773 for j in range(nToUse):
774 # Check for NaNs?
775 kimage = afwImage.ImageD(pcaBasisList[j].getDimensions())
776 pcaBasisList[j].computeImage(kimage, False)
777 if not (True in np.isnan(kimage.array)):
778 trimBasisList.append(pcaBasisList[j])
779
780 # Put all the power in the first kernel, which will not vary spatially
781 spatialBasisList = diffimLib.renormalizeKernelList(trimBasisList)
782
783 # New Kernel visitor for this new basis list (no regularization explicitly)
784 singlekvPca = diffimLib.BuildSingleKernelVisitorF(spatialBasisList, ps)
785 singlekvPca.setSkipBuilt(False)
786 kernelCellSet.visitCandidates(singlekvPca, nStarPerCell)
787 singlekvPca.setSkipBuilt(True)
788 nRejectedPca = singlekvPca.getNRejected()
789
790 return nRejectedPca, spatialBasisList
791
792 @abc.abstractmethod
793 def _buildCellSet(self, *args):
794 """Fill a SpatialCellSet with KernelCandidates for the Psf-matching process;
795 override in derived classes"""
796 return
797
798 @timeMethod
799 def _solve(self, kernelCellSet, basisList):
800 """Solve for the PSF matching kernel
801
802 Parameters
803 ----------
804 kernelCellSet : `lsst.afw.math.SpatialCellSet`
805 a SpatialCellSet to use in determining the matching kernel
806 (typically as provided by _buildCellSet)
807 basisList : `list` of `lsst.afw.math.kernel.FixedKernel`
808 list of Kernels to be used in the decomposition of the spatially varying kernel
809 (typically as provided by makeKernelBasisList)
810
811 Returns
812 -------
813 psfMatchingKernel : `lsst.afw.math.LinearCombinationKernel`
814 Spatially varying Psf-matching kernel
815 backgroundModel : `lsst.afw.math.Function2D`
816 Spatially varying background-matching function
817
818 Raises
819 ------
820 NoKernelCandidatesError :
821 If there are no useable kernel candidates.
822 """
823
824 import lsstDebug
825 display = lsstDebug.Info(__name__).display
826
827 maxSpatialIterations = self.kConfig.maxSpatialIterations
828 nStarPerCell = self.kConfig.nStarPerCell
829 usePcaForSpatialKernel = self.kConfig.usePcaForSpatialKernel
830
831 # Visitor for the single kernel fit
832 ps = pexConfig.makePropertySet(self.kConfig)
833 if self.useRegularization:
834 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps, self.hMat)
835 else:
836 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps)
837
838 # Visitor for the kernel sum rejection
839 ksv = diffimLib.KernelSumVisitorF(ps)
840
841 # Main loop
842 trace_loggers = [getTraceLogger(self.log.getChild("_solve"), i) for i in range(5)]
843 t0 = time.time()
844 totalIterations = 0
845 thisIteration = 0
846 while (thisIteration < maxSpatialIterations):
847
848 # Make sure there are no uninitialized candidates as active occupants of Cell
849 nRejectedSkf = -1
850 while (nRejectedSkf != 0):
851 trace_loggers[1].debug("Building single kernels...")
852 kernelCellSet.visitCandidates(singlekv, nStarPerCell, ignoreExceptions=True)
853 nRejectedSkf = singlekv.getNRejected()
854 trace_loggers[1].debug(
855 "Iteration %d, rejected %d candidates due to initial kernel fit",
856 thisIteration, nRejectedSkf
857 )
858
859 # Reject outliers in kernel sum
860 ksv.resetKernelSum()
861 ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE)
862 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
863
864 allCellsEmpty = True
865 for cell in kernelCellSet.getCellList():
866 if not cell.empty():
867 allCellsEmpty = False
868 break
869 if allCellsEmpty:
870 raise NoKernelCandidatesError("All spatial cells are empty of candidates")
871
872 try:
873 ksv.processKsumDistribution()
874 except RuntimeError as e:
875 raise NoKernelCandidatesError("Unable to calculate the kernel sum") from e
876
877 ksv.setMode(diffimLib.KernelSumVisitorF.REJECT)
878 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
879
880 nRejectedKsum = ksv.getNRejected()
881 trace_loggers[1].debug(
882 "Iteration %d, rejected %d candidates due to kernel sum",
883 thisIteration, nRejectedKsum
884 )
885
886 # Do we jump back to the top without incrementing thisIteration?
887 if nRejectedKsum > 0:
888 totalIterations += 1
889 continue
890
891 # At this stage we can either apply the spatial fit to
892 # the kernels, or we run a PCA, use these as a *new*
893 # basis set with lower dimensionality, and then apply
894 # the spatial fit to these kernels
895
896 if (usePcaForSpatialKernel):
897 trace_loggers[0].debug("Building Pca basis")
898
899 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
900 trace_loggers[1].debug(
901 "Iteration %d, rejected %d candidates due to Pca kernel fit",
902 thisIteration, nRejectedPca
903 )
904
905 # We don't want to continue on (yet) with the
906 # spatial modeling, because we have bad objects
907 # contributing to the Pca basis. We basically
908 # need to restart from the beginning of this loop,
909 # since the cell-mates of those objects that were
910 # rejected need their original Kernels built by
911 # singleKernelFitter.
912
913 # Don't count against thisIteration
914 if (nRejectedPca > 0):
915 totalIterations += 1
916 continue
917 else:
918 spatialBasisList = basisList
919
920 # We have gotten on to the spatial modeling part
921 regionBBox = kernelCellSet.getBBox()
922 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
923 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
924 spatialkv.solveLinearEquation()
925 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
926 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
927
928 # Check the quality of the spatial fit (look at residuals)
929 assesskv = diffimLib.AssessSpatialKernelVisitorF(spatialKernel, spatialBackground, ps)
930 kernelCellSet.visitCandidates(assesskv, nStarPerCell)
931 nRejectedSpatial = assesskv.getNRejected()
932 nGoodSpatial = assesskv.getNGood()
933 trace_loggers[1].debug(
934 "Iteration %d, rejected %d candidates due to spatial kernel fit",
935 thisIteration, nRejectedSpatial
936 )
937 trace_loggers[1].debug("%d candidates used in fit", nGoodSpatial)
938
939 # If only nGoodSpatial == 0, might be other candidates in the cells
940 if nGoodSpatial == 0 and nRejectedSpatial == 0:
941 raise NoKernelCandidatesError("No kernel candidates for spatial fit")
942
943 if nRejectedSpatial == 0:
944 # Nothing rejected, finished with spatial fit
945 break
946
947 # Otherwise, iterate on...
948 thisIteration += 1
949
950 # Final fit if above did not converge
951 if (nRejectedSpatial > 0) and (thisIteration == maxSpatialIterations):
952 trace_loggers[1].debug("Final spatial fit")
953 if (usePcaForSpatialKernel):
954 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
955 regionBBox = kernelCellSet.getBBox()
956 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
957 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
958 spatialkv.solveLinearEquation()
959 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
960 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
961
962 # Run after the final fit to use the updated kernel visitor `spatialkv`
963 if spatialkv.getNCandidates() < 1:
964 raise NoKernelCandidatesError("No kernel candidates remain after max iterations")
965
966 spatialSolution = spatialkv.getKernelSolution()
967
968 t1 = time.time()
969 trace_loggers[0].debug("Total time to compute the spatial kernel : %.2f s", (t1 - t0))
970
971 if display:
972 self._displayDebug(kernelCellSet, spatialKernel, spatialBackground)
973
974 self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground)
975
976 return spatialSolution, spatialKernel, spatialBackground
977
978
979PsfMatch = PsfMatchTask