Coverage for tests/test_psf.py: 14%
201 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-26 16:30 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-26 16:30 +0000
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, debugStarData=False):
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
194 psfDeterminerConfig.debugStarData = debugStarData
196 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)
198 def subtractStars(self, exposure, catalog, chi_lim=-1):
199 """Subtract the exposure's PSF from all the sources in catalog"""
200 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
202 subtracted = mi.Factory(mi, True)
203 for s in catalog:
204 xc, yc = s.getX(), s.getY()
205 bbox = subtracted.getBBox(afwImage.PARENT)
206 if bbox.contains(geom.PointI(int(xc), int(yc))):
207 measAlg.subtractPsf(psf, subtracted, xc, yc)
208 chi = subtracted.Factory(subtracted, True)
209 var = subtracted.getVariance()
210 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
211 chi /= var
213 chi_min = np.min(chi.getImage().getArray())
214 chi_max = np.max(chi.getImage().getArray())
215 print(chi_min, chi_max)
217 if chi_lim > 0:
218 self.assertGreater(chi_min, -chi_lim)
219 self.assertLess(chi_max, chi_lim)
221 def checkPiffDeterminer(self, kernelSize=None, debugStarData=False):
222 """Configure PiffPsfDeterminerTask and run basic tests on it.
224 Parameters
225 ----------
226 kernelSize : `int`, optional
227 Set ``config.kernelSize`` to this, if not None.
228 """
229 self.setupDeterminer(kernelSize=kernelSize, debugStarData=debugStarData)
230 metadata = dafBase.PropertyList()
232 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
233 psfCandidateList = self.makePsfCandidates.run(
234 stars.sourceCat,
235 exposure=self.exposure
236 ).psfCandidates
237 psf, cellSet = self.psfDeterminer.determinePsf(
238 self.exposure,
239 psfCandidateList,
240 metadata,
241 flagKey=self.usePsfFlag
242 )
243 self.exposure.setPsf(psf)
245 self.assertEqual(len(psfCandidateList), metadata['numAvailStars'])
246 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars'])
247 self.assertEqual(
248 psf.getAveragePosition(),
249 geom.Point2D(
250 np.mean([s.x for s in psf._piffResult.stars]),
251 np.mean([s.y for s in psf._piffResult.stars])
252 )
253 )
254 if self.psfDeterminer.config.debugStarData:
255 self.assertIn('image', psf._piffResult.stars[0].data.__dict__)
256 else:
257 self.assertNotIn('image', psf._piffResult.stars[0].data.__dict__)
259 # Test how well we can subtract the PSF model
260 self.subtractStars(self.exposure, self.catalog, chi_lim=6.1)
262 # Test bboxes
263 for point in [
264 psf.getAveragePosition(),
265 geom.Point2D(),
266 geom.Point2D(1, 1)
267 ]:
268 self.assertEqual(
269 psf.computeBBox(point),
270 psf.computeKernelImage(point).getBBox()
271 )
272 self.assertEqual(
273 psf.computeKernelBBox(point),
274 psf.computeKernelImage(point).getBBox()
275 )
276 self.assertEqual(
277 psf.computeImageBBox(point),
278 psf.computeImage(point).getBBox()
279 )
281 # Some roundtrips
282 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
283 self.exposure.writeFits(tmpFile)
284 fitsIm = afwImage.ExposureF(tmpFile)
285 copyIm = copy.deepcopy(self.exposure)
287 for newIm in [fitsIm, copyIm]:
288 # Piff doesn't enable __eq__ for its results, so we just check
289 # that some PSF images come out the same.
290 for point in [
291 geom.Point2D(0, 0),
292 geom.Point2D(10, 100),
293 geom.Point2D(-200, 30),
294 geom.Point2D(float("nan")) # "nullPoint"
295 ]:
296 self.assertImagesAlmostEqual(
297 psf.computeImage(point),
298 newIm.getPsf().computeImage(point)
299 )
300 # Also check average position
301 newPsf = newIm.getPsf()
302 self.assertImagesAlmostEqual(
303 psf.computeImage(psf.getAveragePosition()),
304 newPsf.computeImage(newPsf.getAveragePosition())
305 )
307 def testPiffDeterminer_default(self):
308 """Test piff with the default config."""
309 self.checkPiffDeterminer()
311 def testPiffDeterminer_kernelSize27(self):
312 """Test Piff with a psf kernelSize of 27."""
313 self.checkPiffDeterminer(27)
315 def testPiffDeterminer_debugStarData(self):
316 """Test Piff with debugStarData=True."""
317 self.checkPiffDeterminer(debugStarData=True)
319 @lsst.utils.tests.methodParameters(samplingSize=[1.0, 0.9, 1.1])
320 def test_validatePsfCandidates(self, samplingSize):
321 """Test that `_validatePsfCandidates` raises for too-small candidates.
322 """
323 drawSizeDict = {1.0: 27,
324 0.9: 31,
325 1.1: 25,
326 }
328 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass()
329 makePsfCandidatesConfig.kernelSize = 24
330 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig)
331 psfCandidateList = self.makePsfCandidates.run(
332 self.catalog,
333 exposure=self.exposure
334 ).psfCandidates
336 psfDeterminerConfig = PiffPsfDeterminerConfig()
337 psfDeterminerConfig.kernelSize = 27
338 psfDeterminerConfig.samplingSize = samplingSize
339 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig)
341 with self.assertRaisesRegex(RuntimeError,
342 f"config.kernelSize/config.samplingSize={drawSizeDict[samplingSize]} "
343 "pixels per side; found 24x24"):
344 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27, samplingSize)
346 # This should not raise.
347 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21, samplingSize)
350class PiffConfigTestCase(lsst.utils.tests.TestCase):
351 """A test case to check for valid Piff config"""
352 def testValidateGalsimInterpolant(self):
353 # Check that random strings are not valid interpolants.
354 self.assertFalse(_validateGalsimInterpolant("foo"))
355 # Check that the Lanczos order is an integer
356 self.assertFalse(_validateGalsimInterpolant("Lanczos(3.0"))
357 self.assertFalse(_validateGalsimInterpolant("Lanczos(-5.0"))
358 self.assertFalse(_validateGalsimInterpolant("Lanczos(N)"))
359 # Check for various valid Lanczos interpolants
360 for interp in ("Lanczos(4)", "galsim.Lanczos(7)"):
361 self.assertTrue(_validateGalsimInterpolant(interp))
362 self.assertFalse(_validateGalsimInterpolant(interp.lower()))
363 # Evaluating the string should succeed. This is how Piff does it.
364 self.assertTrue(eval(interp))
365 # Check that interpolation methods are case sensitive.
366 for interp in ("Linear", "Cubic", "Quintic", "Delta", "Nearest", "SincInterpolant"):
367 self.assertFalse(_validateGalsimInterpolant(f"galsim.{interp.lower()}"))
368 self.assertFalse(_validateGalsimInterpolant(interp))
369 self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}"))
370 self.assertTrue(eval(f"galsim.{interp}"))
373class TestMemory(lsst.utils.tests.MemoryTestCase):
374 pass
377def setup_module(module):
378 lsst.utils.tests.init()
381if __name__ == "__main__": 381 ↛ 382line 381 didn't jump to line 382, because the condition on line 381 was never true
382 lsst.utils.tests.init()
383 unittest.main()