-
Notifications
You must be signed in to change notification settings - Fork 8
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
Tickets/dm 9229 #54
Tickets/dm 9229 #54
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ | |
from __future__ import absolute_import, division, print_function | ||
|
||
from builtins import range | ||
import numpy as num | ||
import numpy as np | ||
|
||
from . import diffimLib | ||
import lsst.afw.geom as afwGeom | ||
|
@@ -36,7 +36,14 @@ | |
from . import utils as diUtils | ||
import lsst.afw.display.ds9 as ds9 | ||
|
||
sigma2fwhm = 2. * num.sqrt(2. * num.log(2.)) | ||
__all__ = ("ModelPsfMatchTask", "ModelPsfMatchConfig") | ||
|
||
sigma2fwhm = 2. * np.sqrt(2. * np.log(2.)) | ||
|
||
|
||
def nextOddInteger(x): | ||
nextInt = int(np.ceil(x)) | ||
return nextInt + 1 if nextInt % 2 == 0 else nextInt | ||
|
||
|
||
class ModelPsfMatchConfig(pexConfig.Config): | ||
|
@@ -49,11 +56,27 @@ class ModelPsfMatchConfig(pexConfig.Config): | |
), | ||
default="AL", | ||
) | ||
|
||
padPsf = pexConfig.Field( | ||
doAutoPadPsf = pexConfig.Field( | ||
dtype=bool, | ||
doc="Grow PSF dimensions to largest PSF dimension on input exposure; else clip to smallest", | ||
default=False, | ||
doc=("If too small, automatically pad the science Psf? " | ||
"Pad to smallest dimensions appropriate for the matching kernel dimensions, " | ||
"as specified by autoPadPsfTo. If false, pad by the padPsfBy config."), | ||
default=True, | ||
) | ||
autoPadPsfTo = pexConfig.RangeField( | ||
dtype=float, | ||
doc=("Minimum Science Psf dimensions as a fraction of matching kernel dimensions. " | ||
"If the dimensions of the Psf to be matched are less than the " | ||
"matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. " | ||
"Ignored if doAutoPadPsf=False."), | ||
default=1.4, | ||
min=1.0, | ||
max=2.0 | ||
) | ||
padPsfBy = pexConfig.Field( | ||
dtype=int, | ||
doc="Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True", | ||
default=0, | ||
) | ||
|
||
def setDefaults(self): | ||
|
@@ -288,11 +311,11 @@ def run(self, exposure, referencePsfModel, kernelSum=1.0): | |
spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList) | ||
|
||
if psfMatchingKernel.isSpatiallyVarying(): | ||
sParameters = num.array(psfMatchingKernel.getSpatialParameters()) | ||
sParameters = np.array(psfMatchingKernel.getSpatialParameters()) | ||
sParameters[0][0] = kernelSum | ||
psfMatchingKernel.setSpatialParameters(sParameters) | ||
else: | ||
kParameters = num.array(psfMatchingKernel.getKernelParameters()) | ||
kParameters = np.array(psfMatchingKernel.getKernelParameters()) | ||
kParameters[0] = kernelSum | ||
psfMatchingKernel.setKernelParameters(kParameters) | ||
|
||
|
@@ -344,13 +367,6 @@ def _buildCellSet(self, exposure, referencePsfModel): | |
dimenR = referencePsfModel.getLocalKernel().getDimensions() | ||
psfWidth, psfHeight = dimenR | ||
|
||
maxKernelSize = min(psfWidth, psfHeight) - 1 | ||
if maxKernelSize % 2 == 0: | ||
maxKernelSize -= 1 | ||
if self.kConfig.kernelSize > maxKernelSize: | ||
raise ValueError("Kernel size (%d) too big to match Psfs of size %d; reduce to at least %d" % ( | ||
self.kConfig.kernelSize, psfWidth, maxKernelSize)) | ||
|
||
regionSizeX, regionSizeY = scienceBBox.getDimensions() | ||
scienceX0, scienceY0 = scienceBBox.getMin() | ||
|
||
|
@@ -368,7 +384,6 @@ def _buildCellSet(self, exposure, referencePsfModel): | |
|
||
# Survey the PSF dimensions of the Spatial Cell Set | ||
# to identify the minimum enclosed or maximum bounding square BBox. | ||
# This loop can be made faster with a cheaper method to inspect PSF dimensions | ||
widthList = [] | ||
heightList = [] | ||
for row in range(nCellY): | ||
|
@@ -379,15 +394,42 @@ def _buildCellSet(self, exposure, referencePsfModel): | |
widthList.append(widthS) | ||
heightList.append(heightS) | ||
|
||
lenMax = max(max(heightList), max(widthList)) | ||
lenMin = min(min(heightList), min(widthList)) | ||
psfSize = max(max(heightList), max(widthList)) | ||
|
||
lenPsfScience = lenMax if self.config.padPsf else lenMin | ||
dimenS = afwGeom.Extent2I(lenPsfScience, lenPsfScience) | ||
if self.config.doAutoPadPsf: | ||
minPsfSize = nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo) | ||
paddingPix = max(0, minPsfSize - psfSize) | ||
else: | ||
if self.config.padPsfBy % 2 != 0: | ||
raise ValueError("Config padPsfBy (%i pixels) must be even number." % | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth just moving to the next even number rather than throwing an error? Or maybe being explicit is good, like you have it. If the algorithm must have an even to work, perhaps move to the next even number and put out a warning? What you have is fine, these are just some things for you to consider. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered that. My reasoning for raising an error is that when setting doAutoPadPsf=False, I presume the user has reasons for turning off the automatic tuning, and thus wants exact manual control over the configs and wants ModelPsfMatchTask to just do exactly what they ask of it or quit. The behavior is consistent: Kernel too big? Raise. Padding inappropriately odd? Raise. I suspect this config option will only be used when users want precise control such as for testing config parameters like you did for the HSC image |
||
self.config.padPsfBy) | ||
paddingPix = self.config.padPsfBy | ||
|
||
if paddingPix > 0: | ||
self.log.info("Padding Science PSF from (%s, %s) to (%s, %s) pixels" % | ||
(psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)) | ||
psfSize += paddingPix | ||
|
||
# Check that PSF is larger than the matching kernel | ||
maxKernelSize = psfSize - 1 | ||
if maxKernelSize % 2 == 0: | ||
maxKernelSize -= 1 | ||
if self.kConfig.kernelSize > maxKernelSize: | ||
message = """ | ||
Kernel size (%d) too big to match Psfs of size %d. | ||
Please reconfigure by setting one of the following: | ||
1) kernel size to <= %d | ||
2) doAutoPadPsf=True | ||
3) padPsfBy to >= %s | ||
""" % (self.kConfig.kernelSize, psfSize, | ||
maxKernelSize, self.kConfig.kernelSize - maxKernelSize) | ||
raise ValueError(message) | ||
|
||
dimenS = afwGeom.Extent2I(psfSize, psfSize) | ||
|
||
if (dimenR != dimenS): | ||
self.log.info("Adjusting dimensions of reference PSF model from %s to %s" % (dimenR, dimenS)) | ||
referencePsfModel = referencePsfModel.resized(lenPsfScience, lenPsfScience) | ||
referencePsfModel = referencePsfModel.resized(psfSize, psfSize) | ||
dimenR = dimenS | ||
|
||
policy = pexConfig.makePolicy(self.kConfig) | ||
|
@@ -417,17 +459,10 @@ def _buildCellSet(self, exposure, referencePsfModel): | |
else: | ||
# make image of proper size | ||
kernelImageS = afwImage.ImageF(dimenR) | ||
if self.config.padPsf: | ||
bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((lenMax - rawKernel.getWidth())//2, | ||
(lenMax - rawKernel.getHeight())//2), | ||
rawKernel.getDimensions()) | ||
kernelImageS.assign(rawKernel, bboxToPlace) | ||
else: | ||
bboxToExtract = afwGeom.Box2I(afwGeom.Point2I((rawKernel.getWidth() - lenMin)//2, | ||
(rawKernel.getHeight() - lenMin)//2), | ||
dimenR) | ||
rawKernel.setXY0(0, 0) | ||
kernelImageS = rawKernel.Factory(rawKernel, bboxToExtract) | ||
bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((psfSize - rawKernel.getWidth())//2, | ||
(psfSize - rawKernel.getHeight())//2), | ||
rawKernel.getDimensions()) | ||
kernelImageS.assign(rawKernel, bboxToPlace) | ||
|
||
kernelMaskS = afwImage.MaskU(dimenS) | ||
kernelMaskS.set(0) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
import lsst.ip.diffim as ipDiffim | ||
import lsst.log.utils as logUtils | ||
import lsst.meas.algorithms as measAlg | ||
from lsst.ip.diffim.modelPsfMatch import nextOddInteger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you changed to the binary operator in the other file, remember to change where the function is used here as well |
||
|
||
logUtils.traceSetAt("ip.diffim", 4) | ||
|
||
|
@@ -28,6 +29,10 @@ def setUp(self): | |
self.exp.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize, self.sigma1)) | ||
|
||
def testTooBig(self): | ||
"""Test that modelPsfMatchTask raises if kernel size is too big and | ||
and automatic padding disabled | ||
""" | ||
self.config.doAutoPadPsf = False | ||
self.subconfig.kernelSize = self.ksize | ||
psf = measAlg.DoubleGaussianPsf(self.ksize, self.ksize, self.sigma2) | ||
psfMatch = ipDiffim.ModelPsfMatchTask(config=self.config) | ||
|
@@ -61,13 +66,45 @@ def testAdjustModelSize(self): | |
"""Test that modelPsfMatch correctly adjusts the model PSF dimensions to | ||
match those of the science PSF. | ||
""" | ||
self.config.doAutoPadPsf = False | ||
psfModel = measAlg.DoubleGaussianPsf(self.ksize + 2, self.ksize + 2, self.sigma2) | ||
psfMatch = ipDiffim.ModelPsfMatchTask(config=self.config) | ||
results = psfMatch.run(self.exp, psfModel) | ||
self.assertEqual(results.psfMatchedExposure.getPsf().computeImage().getDimensions(), | ||
self.exp.getPsf().computeImage().getDimensions()) | ||
self.assertEqual(results.psfMatchedExposure.getPsf().getSigma1(), self.sigma2) | ||
|
||
def testPadPsf(self): | ||
"""Test automatic and manual PSF padding | ||
|
||
Compare expected Psf size, after padding, to the reference Psf size. | ||
The reference Psf Size is proxy for the Sciencee Psf size here. | ||
""" | ||
psfModel = measAlg.DoubleGaussianPsf(self.ksize, self.ksize, self.sigma2) | ||
|
||
# Test automatic padding (doAutoPadPsf is True by default) | ||
autoPaddedKernel = nextOddInteger(self.subconfig.kernelSize*self.config.autoPadPsfTo) | ||
psfMatch = ipDiffim.ModelPsfMatchTask(config=self.config) | ||
results = psfMatch.run(self.exp, psfModel) | ||
self.assertEqual(results.psfMatchedExposure.getPsf().computeImage().getWidth(), autoPaddedKernel) | ||
|
||
# Test manual padding | ||
self.config.doAutoPadPsf = False | ||
PAD_EVEN_VALUES = [0, 2, 4] | ||
for padPix in PAD_EVEN_VALUES: | ||
self.config.padPsfBy = padPix | ||
psfMatch = ipDiffim.ModelPsfMatchTask(config=self.config) | ||
results = psfMatch.run(self.exp, psfModel) | ||
self.assertEqual(results.psfMatchedExposure.getPsf().computeImage().getWidth(), | ||
self.ksize + padPix) | ||
|
||
PAD_ODD_VALUES = [1, 3, 5] | ||
for padPix in PAD_ODD_VALUES: | ||
self.config.padPsfBy = padPix | ||
psfMatch = ipDiffim.ModelPsfMatchTask(config=self.config) | ||
with self.assertRaises(ValueError): | ||
results = psfMatch.run(self.exp, psfModel) | ||
|
||
def tearDown(self): | ||
del self.exp | ||
del self.subconfig | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may not be necessary, as this function is only called a small number of times, but it is possible to avoid all the overhead of calling a function, and the steps in the function by saying:
minPsfSize = int(np.ceil(self.KConfig.kernelSize*self.config.autoPadPsfTo))|1
This will return the number if it is odd, or the next integer if it is even
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I acknowledge two suggestions here: (1) to take the minPsfSize calculation out of a function and (2) to switch the last bit to 1 to get the next odd integer.
1: This function is not in a loop and only called once. My rationale for putting it in a function was to reduce code duplication with the unit test. If you argue that unit tests should calculate it differently, then I'll take it out of the function.
2: Flipping the last bit to 1 smells clever to me, because it doesn't read like english.
The function plus english-reading % 2 version also appears to be (counterintuitively) faster, but a microsecond (or even 100 milliseconds) won't make a difference in a long task like ModelPsfMatch:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The speed difference here comes from setupBitFlip using int(np.ceil( and setupIfMod not, it seems to just be doing the mod on whatever type that x is. Over all I don't have a strong feeling on this and reuse in the unit test is a good enough argument for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, whoops. Let me update those last times for posterity: