Skip to content

Commit 7396da9

Browse files
committed
Add thresholds on the model PSF ellipticity in SFP
In order to guard against irregular PSF shapes (e.g the trefoil, trailed, kettlebell, etc. shapes often encountered during commissioning), impose thresholds on the measured PSF model on its ellipticity as well as its unnormalized ellipticity, defined as hypot(Ixx - Iyy, 2Ixy). These thresholds can be set per band and a fallback value is used if the current band is not included in the threshold config dict. Detectors where the initial PSF model has values exceeding these thresholds raise an UnprocessableDataError such that downstream tasks know to skip them (and thus do not result in processing failures that would waste human time chasing down). The values here have been conditioned on the LSSTComCam DP1 dataset and do a good job at only excludeing truly "bad" PSFs. The cuts are intentionally somewhat conservative to allow SFP to continue on PSFs that are more in the "iffy" catagory. For science cases that require strict quality of the PSF, further cuts can be made on the detectors that are included in coaddition.
1 parent 8e3b232 commit 7396da9

File tree

1 file changed

+88
-6
lines changed

1 file changed

+88
-6
lines changed

python/lsst/pipe/tasks/characterizeImage.py

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
import numpy as np
2525

2626
from lsstDebug import getDebugFrame
27+
import lsst.afw.math as afwMath
2728
import lsst.afw.table as afwTable
2829
import lsst.pex.config as pexConfig
2930
import lsst.pipe.base as pipeBase
3031
import lsst.daf.base as dafBase
3132
import lsst.pipe.base.connectionTypes as cT
32-
from lsst.afw.math import BackgroundList
3333
from lsst.afw.table import SourceTable
3434
from lsst.meas.algorithms import (
3535
SubtractBackgroundTask,
@@ -131,6 +131,52 @@ class CharacterizeImageConfig(pipeBase.PipelineTaskConfig,
131131
"estimate PSF. If useSimplePsf is True then 2 should be plenty; "
132132
"otherwise more may be wanted.",
133133
)
134+
maxUnNormPsfEllipticityPerBand = pexConfig.DictField(
135+
keytype=str,
136+
itemtype=float,
137+
default={
138+
"u": 3.8,
139+
"g": 3.8,
140+
"r": 3.8,
141+
"i": 3.8,
142+
"z": 3.8,
143+
"y": 3.8,
144+
},
145+
doc="Maximum unnormalized ellipticity (defined as hypot(Ixx - Iyy, 2Ixy)) of the PSF model "
146+
"deemed good enough for further consideration. Values above this threshold raise "
147+
"UnprocessableDataError.",
148+
)
149+
maxUnNormPsfEllipticityFallback = pexConfig.Field(
150+
dtype=float,
151+
default=3.8,
152+
doc="Fallback maximum unnormalized ellipticity (defined as hypot(Ixx - Iyy, 2Ixy)) of the "
153+
"PSF model deemed good enough for further consideration if the current band is not in "
154+
"the config.maxUnNormPsfEllipticityPerBand dict. Values above this threshold "
155+
"raise UnprocessableDataError.",
156+
)
157+
maxPsfEllipticityPerBand = pexConfig.DictField(
158+
keytype=str,
159+
itemtype=float,
160+
default={
161+
"u": 0.34,
162+
"g": 0.34,
163+
"r": 0.34,
164+
"i": 0.34,
165+
"z": 0.34,
166+
"y": 0.34,
167+
},
168+
doc="Value of the PSF model ellipticity deemed good enough for further consideration, "
169+
"regardless of the value of the unnormalized PSF model ellipticity. Values above "
170+
"this threshold raise UnprocessableDataError.",
171+
)
172+
maxPsfEllipticityFallback = pexConfig.Field(
173+
dtype=float,
174+
default=0.34,
175+
doc="Fallback maximum ellipticity of the PSF model deemed good enough for further "
176+
"consideration if the current band is not in the config.maxPsfEllipticityPerBand "
177+
"dict. Values above this threshold raise UnprocessableDataError.",
178+
)
179+
134180
background = pexConfig.ConfigurableField(
135181
target=SubtractBackgroundTask,
136182
doc="Configuration for initial background estimation",
@@ -403,6 +449,10 @@ def run(self, exposure, background=None, idGenerator=None):
403449
------
404450
RuntimeError
405451
Raised if PSF sigma is NaN.
452+
UnprocessableDataError
453+
Raised if the unnormalized model PSF ellipticity is greater than
454+
maxUnNormPsfEllipticity or the model PSF ellipticity is greater
455+
than maxPsfEllipticity.
406456
"""
407457
self._frame = self._initialFrame # reset debug display frame
408458

@@ -423,17 +473,49 @@ def run(self, exposure, background=None, idGenerator=None):
423473
idGenerator=idGenerator,
424474
background=background,
425475
)
426-
427476
psf = dmeRes.exposure.getPsf()
428477
# Just need a rough estimate; average positions are fine
429478
psfAvgPos = psf.getAveragePosition()
430-
psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
479+
psfShape = psf.computeShape(psfAvgPos)
480+
psfSigma = psfShape.getDeterminantRadius()
481+
psfE1 = (psfShape.getIxx() - psfShape.getIyy())/(psfShape.getIxx() + psfShape.getIyy())
482+
psfE2 = 2.0*psfShape.getIxy()/(psfShape.getIxx() + psfShape.getIyy())
483+
psfE = np.sqrt(psfE1**2.0 + psfE2**2.0)
484+
unNormPsfE = np.hypot(psfShape.getIxx() - psfShape.getIyy(), 2.0*psfShape.getIxy())
485+
431486
psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
432487
medBackground = np.median(dmeRes.background.getImage().getArray())
433-
self.log.info("iter %s; PSF sigma=%0.4f, dimensions=%s; median background=%0.2f",
434-
i + 1, psfSigma, psfDimensions, medBackground)
488+
self.log.info(
489+
"iter %s; PSF sigma=%0.4f, psfE=%.3f, unNormPsfE=%.2f dimensions=%s; "
490+
"median background=%0.2f",
491+
i + 1, psfSigma, psfE, unNormPsfE, psfDimensions, medBackground)
435492
if np.isnan(psfSigma):
436493
raise RuntimeError("PSF sigma is NaN, cannot continue PSF determination.")
494+
band = exposure.filter.bandLabel
495+
if band in self.config.maxUnNormPsfEllipticityPerBand:
496+
maxUnNormPsfEllipticity = self.config.maxUnNormPsfEllipticityPerBand[band]
497+
else:
498+
maxUnNormPsfEllipticity = self.config.maxUnNormPsfEllipticityFallback
499+
self.log.warning(
500+
f"Band {band} was not included in self.config.maxUnNormPsfEllipticityPerBand. "
501+
f"Setting maxUnNormPsfEllipticity to fallback value of {maxUnNormPsfEllipticity}."
502+
)
503+
if band in self.config.maxPsfEllipticityPerBand:
504+
maxPsfEllipticity = self.config.maxPsfEllipticityPerBand[band]
505+
else:
506+
maxPsfEllipticity = self.config.maxPsfEllipticityFallback
507+
self.log.warning(
508+
f"Band {band} was not included in self.config.maxPsfEllipticityPerBand. "
509+
f"Setting maxUnNormPsfEllipticity to fallback value of {maxPsfEllipticity:.2f}."
510+
)
511+
512+
if unNormPsfE > maxUnNormPsfEllipticity or psfE > maxPsfEllipticity:
513+
raise pipeBase.UnprocessableDataError(
514+
"Either the unnormalized model PSF ellipticity is greater than the maximum allowed "
515+
f"for band {band} ({maxUnNormPsfEllipticity:.2f}) or the model PSF ellipticity "
516+
f"is greater than the maximum allowed({maxPsfEllipticity:.2f})"
517+
f"(unNormPsfE = {unNormPsfE:.2f}, psfE = {psfE:.2f})"
518+
)
437519

438520
self.display("psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
439521

@@ -558,7 +640,7 @@ def detectMeasureAndEstimatePsf(self, exposure, idGenerator, background):
558640
self.display("repair_iter", exposure=exposure)
559641

560642
if background is None:
561-
background = BackgroundList()
643+
background = afwMath.BackgroundList()
562644

563645
sourceIdFactory = idGenerator.make_table_id_factory()
564646
table = SourceTable.make(self.schema, sourceIdFactory)

0 commit comments

Comments
 (0)