Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-36071: Deprecate kernelSize and switch to stampSize #16

Merged
merged 7 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 35 additions & 41 deletions python/lsst/meas/extensions/piff/piffPsfDeterminer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,61 +67,52 @@ def _validateGalsimInterpolant(name: str) -> bool:


class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass):
spatialOrder = pexConfig.Field(
spatialOrder = pexConfig.Field[int](
doc="specify spatial order for PSF kernel creation",
dtype=int,
default=2,
)
samplingSize = pexConfig.Field(
samplingSize = pexConfig.Field[float](
doc="Resolution of the internal PSF model relative to the pixel size; "
"e.g. 0.5 is equal to 2x oversampling",
dtype=float,
default=1,
)
outlierNSigma = pexConfig.Field(
outlierNSigma = pexConfig.Field[float](
doc="n sigma for chisq outlier rejection",
dtype=float,
default=4.0
)
outlierMaxRemove = pexConfig.Field(
outlierMaxRemove = pexConfig.Field[float](
doc="Max fraction of stars to remove as outliers each iteration",
dtype=float,
default=0.05
)
maxSNR = pexConfig.Field(
maxSNR = pexConfig.Field[float](
doc="Rescale the weight of bright stars such that their SNR is less "
"than this value.",
dtype=float,
default=200.0
)
zeroWeightMaskBits = pexConfig.ListField(
zeroWeightMaskBits = pexConfig.ListField[str](
doc="List of mask bits for which to set pixel weights to zero.",
dtype=str,
default=['BAD', 'CR', 'INTRP', 'SAT', 'SUSPECT', 'NO_DATA']
)
minimumUnmaskedFraction = pexConfig.Field(
minimumUnmaskedFraction = pexConfig.Field[float](
doc="Minimum fraction of unmasked pixels required to use star.",
dtype=float,
default=0.5
)
interpolant = pexConfig.Field(
interpolant = pexConfig.Field[str](
doc="GalSim interpolant name for Piff to use. "
"Options include 'Lanczos(N)', where N is an integer, along with "
"galsim.Cubic, galsim.Delta, galsim.Linear, galsim.Nearest, "
"galsim.Quintic, and galsim.SincInterpolant.",
dtype=str,
check=_validateGalsimInterpolant,
default="Lanczos(11)",
)

def setDefaults(self):
# kernelSize should be at least 25 so that
super().setDefaults()
# stampSize should be at least 25 so that
# i) aperture flux with 12 pixel radius can be compared to PSF flux.
# ii) fake sources injected to match the 12 pixel aperture flux get
# measured correctly
self.kernelSize = 25
self.kernelSizeMin = 11
self.kernelSizeMax = 35
self.stampSize = 25


def getGoodPixels(maskedImage, zeroWeightMaskBits):
Expand Down Expand Up @@ -285,16 +276,20 @@ def determinePsf(
psfCellSet : `None`
Unused by this PsfDeterminer.
"""
kernelSize = int(np.clip(
self.config.kernelSize,
self.config.kernelSizeMin,
self.config.kernelSizeMax
))
self._validatePsfCandidates(psfCandidateList, kernelSize, self.config.samplingSize)
if self.config.stampSize:
stampSize = self.config.stampSize
if stampSize > psfCandidateList[0].getWidth():
self.log.warning("stampSize is larger than the PSF candidate size. Using candidate size.")
stampSize = psfCandidateList[0].getWidth()
else: # TODO: Only the if block should stay after DM-36311
self.log.debug("stampSize not set. Using candidate size.")
stampSize = psfCandidateList[0].getWidth()

self._validatePsfCandidates(psfCandidateList, stampSize)

stars = []
for candidate in psfCandidateList:
cmi = candidate.getMaskedImage()
cmi = candidate.getMaskedImage(stampSize, stampSize)
good = getGoodPixels(cmi, self.config.zeroWeightMaskBits)
fracGood = np.sum(good)/good.size
if fracGood < self.config.minimumUnmaskedFraction:
Expand Down Expand Up @@ -326,7 +321,7 @@ def determinePsf(
'model': {
'type': 'PixelGrid',
'scale': self.config.samplingSize,
'size': kernelSize,
'size': stampSize,
'interp': self.config.interpolant
},
'interp': {
Expand All @@ -346,7 +341,7 @@ def determinePsf(
pointing = None

piffResult.fit(stars, wcs, pointing, logger=self.log)
drawSize = 2*np.floor(0.5*kernelSize/self.config.samplingSize) + 1
drawSize = 2*np.floor(0.5*stampSize/self.config.samplingSize) + 1
psf = PiffPsf(drawSize, drawSize, piffResult)

used_image_pos = [s.image_pos for s in piffResult.stars]
Expand All @@ -366,32 +361,31 @@ def determinePsf(

return psf, None

def _validatePsfCandidates(self, psfCandidateList, kernelSize, samplingSize):
# TODO: DM-36311: This method can be removed.
@staticmethod
def _validatePsfCandidates(psfCandidateList, stampSize):
"""Raise if psfCandidates are smaller than the configured kernelSize.

Parameters
----------
psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate`
Sequence of psf candidates to check.
kernelSize : `int`
stampSize : `int`
Size of image model to use in PIFF.
samplingSize : `float`
Resolution of the internal PSF model relative to the pixel size.

Raises
------
RuntimeError
Raised if any psfCandidate has width or height smaller than
config.kernelSize.
``stampSize``.
"""
# We can assume all candidates have the same dimensions.
# All candidates will necessarily have the same dimensions.
candidate = psfCandidateList[0]
drawSize = int(2*np.floor(0.5*kernelSize/samplingSize) + 1)
if (candidate.getHeight() < drawSize
or candidate.getWidth() < drawSize):
raise RuntimeError("PSF candidates must be at least config.kernelSize/config.samplingSize="
f"{drawSize} pixels per side; "
f"found {candidate.getWidth()}x{candidate.getHeight()}.")
if (candidate.getHeight() < stampSize
or candidate.getWidth() < stampSize):
raise RuntimeError(f"PSF candidates must be at least {stampSize=} pixels per side; "
f"found {candidate.getWidth()}x{candidate.getHeight()}."
)


measAlg.psfDeterminerRegistry.register("piff", PiffPsfDeterminerTask)
60 changes: 43 additions & 17 deletions tests/test_psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from lsst.meas.base import SingleFrameMeasurementTask
from lsst.meas.extensions.piff.piffPsfDeterminer import PiffPsfDeterminerConfig, PiffPsfDeterminerTask
from lsst.meas.extensions.piff.piffPsfDeterminer import _validateGalsimInterpolant
from lsst.pex.config import FieldValidationError


def psfVal(ix, iy, x, y, sigma1, sigma2, b):
Expand Down Expand Up @@ -160,13 +161,13 @@ def setUp(self):
cand = measAlg.makePsfCandidate(source, self.exposure)
self.cellSet.insertCandidate(cand)

def setupDeterminer(self, kernelSize=None):
def setupDeterminer(self, stampSize=None):
"""Setup the starSelector and psfDeterminer

Parameters
----------
kernelSize : `int`, optional
Set ``config.kernelSize`` to this, if not None.
stampSize : `int`, optional
Set ``config.stampSize`` to this, if not None.
"""
starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
starSelectorConfig = starSelectorClass.ConfigClass()
Expand All @@ -183,14 +184,14 @@ def setupDeterminer(self, kernelSize=None):
self.starSelector = starSelectorClass(config=starSelectorConfig)

makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
if kernelSize is not None:
makePsfCandidatesConfig.kernelSize = kernelSize
if stampSize is not None:
makePsfCandidatesConfig.kernelSize = stampSize
self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)

psfDeterminerConfig = PiffPsfDeterminerConfig()
psfDeterminerConfig.spatialOrder = 1
if kernelSize is not None:
psfDeterminerConfig.kernelSize = kernelSize
if stampSize is not None:
psfDeterminerConfig.stampSize = stampSize

self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)

Expand All @@ -217,15 +218,15 @@ def subtractStars(self, exposure, catalog, chi_lim=-1):
self.assertGreater(chi_min, -chi_lim)
self.assertLess(chi_max, chi_lim)

def checkPiffDeterminer(self, kernelSize=None):
def checkPiffDeterminer(self, stampSize=None):
"""Configure PiffPsfDeterminerTask and run basic tests on it.

Parameters
----------
kernelSize : `int`, optional
Set ``config.kernelSize`` to this, if not None.
stampSize : `int`, optional
Set ``config.stampSize`` to this, if not None.
"""
self.setupDeterminer(kernelSize=kernelSize)
self.setupDeterminer(stampSize=stampSize)
metadata = dafBase.PropertyList()

stars = self.starSelector.run(self.catalog, exposure=self.exposure)
Expand Down Expand Up @@ -310,32 +311,34 @@ def testPiffDeterminer_kernelSize27(self):
@lsst.utils.tests.methodParameters(samplingSize=[1.0, 0.9, 1.1])
def test_validatePsfCandidates(self, samplingSize):
"""Test that `_validatePsfCandidates` raises for too-small candidates.

This should be independent of the samplingSize parameter.
"""
drawSizeDict = {1.0: 27,
0.9: 31,
1.1: 25,
}

makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
makePsfCandidatesConfig.kernelSize = 24
makePsfCandidatesConfig.kernelSize = 23
self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)
psfCandidateList = self.makePsfCandidates.run(
self.catalog,
exposure=self.exposure
).psfCandidates

psfDeterminerConfig = PiffPsfDeterminerConfig()
psfDeterminerConfig.kernelSize = 27
psfDeterminerConfig.stampSize = drawSizeDict[samplingSize]
psfDeterminerConfig.samplingSize = samplingSize
self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)

with self.assertRaisesRegex(RuntimeError,
f"config.kernelSize/config.samplingSize={drawSizeDict[samplingSize]} "
"pixels per side; found 24x24"):
self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27, samplingSize)
"stampSize=27 "
"pixels per side; found 23x23"):
self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27)

# This should not raise.
self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21, samplingSize)
self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21)


class PiffConfigTestCase(lsst.utils.tests.TestCase):
Expand All @@ -360,6 +363,29 @@ def testValidateGalsimInterpolant(self):
self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}"))
self.assertTrue(eval(f"galsim.{interp}"))

def testKernelSize(self): # TODO: Remove this test in DM-36311.
config = PiffPsfDeterminerConfig()

# Setting both stampSize and kernelSize should raise an error.
config.stampSize = 27
with self.assertWarns(FutureWarning):
config.kernelSize = 25
self.assertRaises(FieldValidationError, config.validate)

# even if they agree with each other
config.stampSize = 31
with self.assertWarns(FutureWarning):
config.kernelSize = 31
self.assertRaises(FieldValidationError, config.validate)

# Setting stampSize and kernelSize should be valid, because if not
# set, stampSize is set to the size of PSF candidates internally.
# This is only a temporary behavior and should go away in DM-36311.
config.stampSize = None
with self.assertWarns(FutureWarning):
config.kernelSize = None
config.validate()


class TestMemory(lsst.utils.tests.MemoryTestCase):
pass
Expand Down