diff --git a/python/lsst/meas/extensions/piff/piffPsfDeterminer.py b/python/lsst/meas/extensions/piff/piffPsfDeterminer.py index 5560df1..54be841 100644 --- a/python/lsst/meas/extensions/piff/piffPsfDeterminer.py +++ b/python/lsst/meas/extensions/piff/piffPsfDeterminer.py @@ -26,6 +26,7 @@ import galsim import re import logging +import yaml import lsst.utils.logging from lsst.afw.cameraGeom import PIXELS, FIELD_ANGLE @@ -72,24 +73,29 @@ def _validateGalsimInterpolant(name: str) -> bool: class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass): spatialOrder = pexConfig.Field[int]( - doc="specify spatial order for PSF kernel creation", + doc="Spatial order for PSF kernel creation. " + "Ignored if piffPsfConfigYaml is set.", default=2, ) 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", + "e.g. 0.5 is equal to 2x oversampling. This affects only the size of " + "the PSF model stamp if piffPsfConfigYaml is set.", default=1, ) modelSize = pexConfig.Field[int]( - doc="Internal model size for PIFF (typically odd, but not enforced)", + doc="Internal model size for PIFF (typically odd, but not enforced). " + "Partially ignored if piffPsfConfigYaml is set.", default=25, ) outlierNSigma = pexConfig.Field[float]( - doc="n sigma for chisq outlier rejection", + doc="n sigma for chisq outlier rejection. " + "Ignored if piffPsfConfigYaml is set.", default=4.0 ) outlierMaxRemove = pexConfig.Field[float]( - doc="Max fraction of stars to remove as outliers each iteration", + doc="Max fraction of stars to remove as outliers each iteration. " + "Ignored if piffPsfConfigYaml is set.", default=0.05 ) maxSNR = pexConfig.Field[float]( @@ -109,7 +115,8 @@ class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass): 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.", + "galsim.Quintic, and galsim.SincInterpolant. Ignored if " + "piffPsfConfigYaml is set.", check=_validateGalsimInterpolant, default="Lanczos(11)", ) @@ -134,6 +141,12 @@ class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass): min=0, max=3, ) + piffPsfConfigYaml = pexConfig.Field[str]( + doc="Configuration file for PIFF in YAML format. This overrides many " + "other settings in this config and is not validated ahead of runtime.", + default=None, + optional=True, + ) def setDefaults(self): super().setDefaults() @@ -410,24 +423,27 @@ def determinePsf( ) stars.append(piff.Star(data, None)) - piffConfig = { - 'type': "Simple", - 'model': { - 'type': 'PixelGrid', - 'scale': scale * self.config.samplingSize, - 'size': self.config.modelSize, - 'interp': self.config.interpolant - }, - 'interp': { - 'type': 'BasisPolynomial', - 'order': self.config.spatialOrder - }, - 'outliers': { - 'type': 'Chisq', - 'nsigma': self.config.outlierNSigma, - 'max_remove': self.config.outlierMaxRemove + if self.config.piffPsfConfigYaml is None: + piffConfig = { + 'type': 'Simple', + 'model': { + 'type': 'PixelGrid', + 'scale': scale * self.config.samplingSize, + 'size': self.config.modelSize, + 'interp': self.config.interpolant, + }, + 'interp': { + 'type': 'BasisPolynomial', + 'order': self.config.spatialOrder, + }, + 'outliers': { + 'type': 'Chisq', + 'nsigma': self.config.outlierNSigma, + 'max_remove': self.config.outlierMaxRemove, + } } - } + else: + piffConfig = yaml.safe_load(self.config.piffPsfConfigYaml) piffResult = piff.PSF.process(piffConfig) wcs = {0: gswcs} diff --git a/tests/test_psf.py b/tests/test_psf.py index 291c470..7b2d49d 100644 --- a/tests/test_psf.py +++ b/tests/test_psf.py @@ -232,6 +232,7 @@ def setupDeterminer( modelSize=25, debugStarData=False, useCoordinates='pixel', + piffPsfConfigYaml=None, downsample=False, withlog=False, ): @@ -284,6 +285,7 @@ def setupDeterminer( psfDeterminerConfig.debugStarData = debugStarData psfDeterminerConfig.useCoordinates = useCoordinates + psfDeterminerConfig.piffPsfConfigYaml = piffPsfConfigYaml if downsample: psfDeterminerConfig.maxCandidates = 10 if withlog: @@ -474,6 +476,10 @@ def testPiffDeterminer_default(self): def testPiffDeterminer_stampSize27(self): """Test Piff with a psf stampSize of 27.""" self.checkPiffDeterminer(stampSize=27) + self.assertEqual( + self.exposure.psf.computeKernelImage(self.exposure.getBBox().getCenter()).getDimensions(), + geom.Extent2I(27, 27), + ) def testPiffDeterminer_debugStarData(self): """Test Piff with debugStarData=True.""" @@ -518,6 +524,32 @@ def testPiffDeterminer_skyCoords_failure(self, angle_degrees=135): self.checkPiffDeterminer(useCoordinates='sky', stampSize=15) +class piffPsfConfigYamlTestCase(SpatialModelPsfTestCase): + """A test case to trigger the codepath that uses piffPsfConfigYaml.""" + + def checkPiffDeterminer(self, **kwargs): + # Docstring inherited. + if "piffPsfConfigYaml" not in kwargs: + piffPsfConfigYaml = """ + # A minimal Piff config corresponding to the defaults. + type: Simple + model: + type: PixelGrid + scale: 0.2 + size: 25 + interp: Lanczos(11) + interp: + type: BasisPolynomial + order: 1 + outliers: + type: Chisq + nsigma: 4.0 + max_remove: 0.05 + """ + kwargs["piffPsfConfigYaml"] = piffPsfConfigYaml + return super().checkPiffDeterminer(**kwargs) + + class PiffConfigTestCase(lsst.utils.tests.TestCase): """A test case to check for valid Piff config""" def testValidateGalsimInterpolant(self):