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