Coverage for tests/test_psf.py: 15%
195 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 13:26 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 13:26 -0800
1import galsim # noqa: F401
2import unittest
3import numpy as np
4import copy
5from galsim import Lanczos # noqa: F401
7import lsst.utils.tests
8import lsst.afw.detection as afwDetection
9import lsst.afw.geom as afwGeom
10import lsst.afw.image as afwImage
11import lsst.afw.math as afwMath
12import lsst.afw.table as afwTable
13import lsst.daf.base as dafBase
14import lsst.geom as geom
15import lsst.meas.algorithms as measAlg
16from lsst.meas.base import SingleFrameMeasurementTask
17from lsst.meas.extensions.piff.piffPsfDeterminer import PiffPsfDeterminerConfig, PiffPsfDeterminerTask
18from lsst.meas.extensions.piff.piffPsfDeterminer import _validateGalsimInterpolant
21def psfVal(ix, iy, x, y, sigma1, sigma2, b):
22 """Return the value at (ix, iy) of a double Gaussian
23 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b)
24 centered at (x, y)
25 """
26 dx, dy = x - ix, y - iy
27 theta = np.radians(30)
28 ab = 1.0/0.75 # axis ratio
29 c, s = np.cos(theta), np.sin(theta)
30 u, v = c*dx - s*dy, s*dx + c*dy
32 return (np.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2)
33 + b*np.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b)
36class SpatialModelPsfTestCase(lsst.utils.tests.TestCase):
37 """A test case for SpatialModelPsf"""
39 def measure(self, footprintSet, exposure):
40 """Measure a set of Footprints, returning a SourceCatalog"""
41 catalog = afwTable.SourceCatalog(self.schema)
43 footprintSet.makeSources(catalog)
45 self.measureSources.run(catalog, exposure)
46 return catalog
48 def setUp(self):
49 config = SingleFrameMeasurementTask.ConfigClass()
50 config.slots.apFlux = 'base_CircularApertureFlux_12_0'
51 self.schema = afwTable.SourceTable.makeMinimalSchema()
53 self.measureSources = SingleFrameMeasurementTask(
54 self.schema, config=config
55 )
56 self.usePsfFlag = self.schema.addField("use_psf", type="Flag")
58 width, height = 110, 301
60 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height))
61 self.mi.set(0)
62 sd = 3 # standard deviation of image
63 self.mi.getVariance().set(sd*sd)
64 self.mi.getMask().addMaskPlane("DETECTED")
66 self.ksize = 31 # size of desired kernel
68 sigma1 = 1.75
69 sigma2 = 2*sigma1
71 self.exposure = afwImage.makeExposure(self.mi)
72 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize,
73 1.5*sigma1, 1, 0.1))
74 cdMatrix = np.array([1.0, 0.0, 0.0, 1.0])
75 cdMatrix.shape = (2, 2)
76 wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0),
77 crval=geom.SpherePoint(0.0, 0.0, geom.degrees),
78 cdMatrix=cdMatrix)
79 self.exposure.setWcs(wcs)
81 #
82 # Make a kernel with the exactly correct basis functions.
83 # Useful for debugging
84 #
85 basisKernelList = []
86 for sigma in (sigma1, sigma2):
87 basisKernel = afwMath.AnalyticKernel(
88 self.ksize, self.ksize, afwMath.GaussianFunction2D(sigma, sigma)
89 )
90 basisImage = afwImage.ImageD(basisKernel.getDimensions())
91 basisKernel.computeImage(basisImage, True)
92 basisImage /= np.sum(basisImage.getArray())
94 if sigma == sigma1:
95 basisImage0 = basisImage
96 else:
97 basisImage -= basisImage0
99 basisKernelList.append(afwMath.FixedKernel(basisImage))
101 order = 1 # 1 => up to linear
102 spFunc = afwMath.PolynomialFunction2D(order)
104 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
105 exactKernel.setSpatialParameters(
106 [[1.0, 0, 0],
107 [0.0, 0.5*1e-2, 0.2e-2]]
108 )
110 rand = afwMath.Random() # make these tests repeatable by setting seed
112 im = self.mi.getImage()
113 afwMath.randomGaussianImage(im, rand) # N(0, 1)
114 im *= sd # N(0, sd^2)
116 xarr, yarr = [], []
118 for x, y in [(20, 20), (60, 20),
119 (30, 35),
120 (50, 50),
121 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
122 (50, 120), (70, 80),
123 (60, 210), (20, 210),
124 ]:
125 xarr.append(x)
126 yarr.append(y)
128 for x, y in zip(xarr, yarr):
129 dx = rand.uniform() - 0.5 # random (centered) offsets
130 dy = rand.uniform() - 0.5
132 k = exactKernel.getSpatialFunction(1)(x, y)
133 b = (k*sigma1**2/((1 - k)*sigma2**2))
135 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
136 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
137 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
138 if iy < 0 or iy >= self.mi.getHeight():
139 continue
141 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
142 if ix < 0 or ix >= self.mi.getWidth():
143 continue
145 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
146 Isample = rand.poisson(II)
147 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
148 self.mi.variance[ix, iy, afwImage.LOCAL] += II
150 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
151 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
153 self.footprintSet = afwDetection.FootprintSet(
154 self.mi, afwDetection.Threshold(100), "DETECTED"
155 )
157 self.catalog = self.measure(self.footprintSet, self.exposure)
159 for source in self.catalog:
160 cand = measAlg.makePsfCandidate(source, self.exposure)
161 self.cellSet.insertCandidate(cand)
163 def setupDeterminer(self, kernelSize=None):
164 """Setup the starSelector and psfDeterminer
166 Parameters
167 ----------
168 kernelSize : `int`, optional
169 Set ``config.kernelSize`` to this, if not None.
170 """
171 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
172 starSelectorConfig = starSelectorClass.ConfigClass()
173 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
174 starSelectorConfig.badFlags = [
175 "base_PixelFlags_flag_edge",
176 "base_PixelFlags_flag_interpolatedCenter",
177 "base_PixelFlags_flag_saturatedCenter",
178 "base_PixelFlags_flag_crCenter",
179 ]
180 # Set to match when the tolerance of the test was set
181 starSelectorConfig.widthStdAllowed = 0.5
183 self.starSelector = starSelectorClass(config=starSelectorConfig)
185 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
186 if kernelSize is not None:
187 makePsfCandidatesConfig.kernelSize = kernelSize
188 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)
190 psfDeterminerConfig = PiffPsfDeterminerConfig()
191 psfDeterminerConfig.spatialOrder = 1
192 if kernelSize is not None:
193 psfDeterminerConfig.kernelSize = kernelSize
195 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)
197 def subtractStars(self, exposure, catalog, chi_lim=-1):
198 """Subtract the exposure's PSF from all the sources in catalog"""
199 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
201 subtracted = mi.Factory(mi, True)
202 for s in catalog:
203 xc, yc = s.getX(), s.getY()
204 bbox = subtracted.getBBox(afwImage.PARENT)
205 if bbox.contains(geom.PointI(int(xc), int(yc))):
206 measAlg.subtractPsf(psf, subtracted, xc, yc)
207 chi = subtracted.Factory(subtracted, True)
208 var = subtracted.getVariance()
209 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
210 chi /= var
212 chi_min = np.min(chi.getImage().getArray())
213 chi_max = np.max(chi.getImage().getArray())
214 print(chi_min, chi_max)
216 if chi_lim > 0:
217 self.assertGreater(chi_min, -chi_lim)
218 self.assertLess(chi_max, chi_lim)
220 def checkPiffDeterminer(self, kernelSize=None):
221 """Configure PiffPsfDeterminerTask and run basic tests on it.
223 Parameters
224 ----------
225 kernelSize : `int`, optional
226 Set ``config.kernelSize`` to this, if not None.
227 """
228 self.setupDeterminer(kernelSize=kernelSize)
229 metadata = dafBase.PropertyList()
231 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
232 psfCandidateList = self.makePsfCandidates.run(
233 stars.sourceCat,
234 exposure=self.exposure
235 ).psfCandidates
236 psf, cellSet = self.psfDeterminer.determinePsf(
237 self.exposure,
238 psfCandidateList,
239 metadata,
240 flagKey=self.usePsfFlag
241 )
242 self.exposure.setPsf(psf)
244 self.assertEqual(len(psfCandidateList), metadata['numAvailStars'])
245 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars'])
246 self.assertEqual(
247 psf.getAveragePosition(),
248 geom.Point2D(
249 np.mean([s.x for s in psf._piffResult.stars]),
250 np.mean([s.y for s in psf._piffResult.stars])
251 )
252 )
254 # Test how well we can subtract the PSF model
255 self.subtractStars(self.exposure, self.catalog, chi_lim=6.1)
257 # Test bboxes
258 for point in [
259 psf.getAveragePosition(),
260 geom.Point2D(),
261 geom.Point2D(1, 1)
262 ]:
263 self.assertEqual(
264 psf.computeBBox(point),
265 psf.computeKernelImage(point).getBBox()
266 )
267 self.assertEqual(
268 psf.computeKernelBBox(point),
269 psf.computeKernelImage(point).getBBox()
270 )
271 self.assertEqual(
272 psf.computeImageBBox(point),
273 psf.computeImage(point).getBBox()
274 )
276 # Some roundtrips
277 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
278 self.exposure.writeFits(tmpFile)
279 fitsIm = afwImage.ExposureF(tmpFile)
280 copyIm = copy.deepcopy(self.exposure)
282 for newIm in [fitsIm, copyIm]:
283 # Piff doesn't enable __eq__ for its results, so we just check
284 # that some PSF images come out the same.
285 for point in [
286 geom.Point2D(0, 0),
287 geom.Point2D(10, 100),
288 geom.Point2D(-200, 30),
289 geom.Point2D(float("nan")) # "nullPoint"
290 ]:
291 self.assertImagesAlmostEqual(
292 psf.computeImage(point),
293 newIm.getPsf().computeImage(point)
294 )
295 # Also check average position
296 newPsf = newIm.getPsf()
297 self.assertImagesAlmostEqual(
298 psf.computeImage(psf.getAveragePosition()),
299 newPsf.computeImage(newPsf.getAveragePosition())
300 )
302 def testPiffDeterminer_default(self):
303 """Test piff with the default config."""
304 self.checkPiffDeterminer()
306 def testPiffDeterminer_kernelSize27(self):
307 """Test Piff with a psf kernelSize of 27."""
308 self.checkPiffDeterminer(27)
310 @lsst.utils.tests.methodParameters(samplingSize=[1.0, 0.9, 1.1])
311 def test_validatePsfCandidates(self, samplingSize):
312 """Test that `_validatePsfCandidates` raises for too-small candidates.
313 """
314 drawSizeDict = {1.0: 27,
315 0.9: 31,
316 1.1: 25,
317 }
319 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
320 makePsfCandidatesConfig.kernelSize = 24
321 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)
322 psfCandidateList = self.makePsfCandidates.run(
323 self.catalog,
324 exposure=self.exposure
325 ).psfCandidates
327 psfDeterminerConfig = PiffPsfDeterminerConfig()
328 psfDeterminerConfig.kernelSize = 27
329 psfDeterminerConfig.samplingSize = samplingSize
330 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)
332 with self.assertRaisesRegex(RuntimeError,
333 f"config.kernelSize/config.samplingSize={drawSizeDict[samplingSize]} "
334 "pixels per side; found 24x24"):
335 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27, samplingSize)
337 # This should not raise.
338 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21, samplingSize)
341class PiffConfigTestCase(lsst.utils.tests.TestCase):
342 """A test case to check for valid Piff config"""
343 def testValidateGalsimInterpolant(self):
344 # Check that random strings are not valid interpolants.
345 self.assertFalse(_validateGalsimInterpolant("foo"))
346 # Check that the Lanczos order is an integer
347 self.assertFalse(_validateGalsimInterpolant("Lanczos(3.0"))
348 self.assertFalse(_validateGalsimInterpolant("Lanczos(-5.0"))
349 self.assertFalse(_validateGalsimInterpolant("Lanczos(N)"))
350 # Check for various valid Lanczos interpolants
351 for interp in ("Lanczos(4)", "galsim.Lanczos(7)"):
352 self.assertTrue(_validateGalsimInterpolant(interp))
353 self.assertFalse(_validateGalsimInterpolant(interp.lower()))
354 # Evaluating the string should succeed. This is how Piff does it.
355 self.assertTrue(eval(interp))
356 # Check that interpolation methods are case sensitive.
357 for interp in ("Linear", "Cubic", "Quintic", "Delta", "Nearest", "SincInterpolant"):
358 self.assertFalse(_validateGalsimInterpolant(f"galsim.{interp.lower()}"))
359 self.assertFalse(_validateGalsimInterpolant(interp))
360 self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}"))
361 self.assertTrue(eval(f"galsim.{interp}"))
364class TestMemory(lsst.utils.tests.MemoryTestCase):
365 pass
368def setup_module(module):
369 lsst.utils.tests.init()
372if __name__ == "__main__": 372 ↛ 373line 372 didn't jump to line 373, because the condition on line 372 was never true
373 lsst.utils.tests.init()
374 unittest.main()