Coverage for tests/test_PsfexPsf.py: 16%
167 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-10 02:48 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-10 02:48 -0800
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23import math
24import numpy as np
25import unittest
27import lsst.utils.tests
28import lsst.afw.image as afwImage
29import lsst.afw.detection as afwDetection
30import lsst.afw.geom as afwGeom
31import lsst.geom as geom
32import lsst.afw.math as afwMath
33import lsst.afw.table as afwTable
34import lsst.daf.base as dafBase
35import lsst.meas.algorithms as measAlg
36from lsst.meas.base import SingleFrameMeasurementTask
37# register the PSF determiner
38import lsst.meas.extensions.psfex.psfexPsfDeterminer
39assert lsst.meas.extensions.psfex.psfexPsfDeterminer # make pyflakes happy
41try:
42 display
43except NameError:
44 display = False
45else:
46 import lsst.afw.display as afwDisplay
47 afwDisplay.setDefaultMaskTransparency(75)
50def psfVal(ix, iy, x, y, sigma1, sigma2, b):
51 """Return the value at (ix, iy) of a double Gaussian
52 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b)
53 centered at (x, y)
54 """
55 dx, dy = x - ix, y - iy
56 theta = np.radians(30)
57 ab = 1.0/0.75 # axis ratio
58 c, s = np.cos(theta), np.sin(theta)
59 u, v = c*dx - s*dy, s*dx + c*dy
61 return (math.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2)
62 + b*math.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b)
65class SpatialModelPsfTestCase(unittest.TestCase):
66 """A test case for SpatialModelPsf"""
68 def measure(self, footprintSet, exposure):
69 """Measure a set of Footprints, returning a SourceCatalog"""
70 catalog = afwTable.SourceCatalog(self.schema)
71 if display:
72 afwDisplay.Display(frame=0).mtv(exposure, title="Original")
74 footprintSet.makeSources(catalog)
76 self.measureSources.run(catalog, exposure)
77 return catalog
79 def setUp(self):
80 config = SingleFrameMeasurementTask.ConfigClass()
81 config.slots.apFlux = 'base_CircularApertureFlux_12_0'
82 self.schema = afwTable.SourceTable.makeMinimalSchema()
84 self.measureSources = SingleFrameMeasurementTask(self.schema, config=config)
86 width, height = 110, 301
88 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height))
89 self.mi.set(0)
90 sd = 3 # standard deviation of image
91 self.mi.getVariance().set(sd*sd)
92 self.mi.getMask().addMaskPlane("DETECTED")
94 self.ksize = 31 # size of desired kernel
96 sigma1 = 1.75
97 sigma2 = 2*sigma1
99 self.exposure = afwImage.makeExposure(self.mi)
100 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize,
101 1.5*sigma1, 1, 0.1))
102 cdMatrix = np.array([1.0, 0.0, 0.0, 1.0])
103 cdMatrix.shape = (2, 2)
104 wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0),
105 crval=geom.SpherePoint(0.0, 0.0, geom.degrees),
106 cdMatrix=cdMatrix)
107 self.exposure.setWcs(wcs)
109 #
110 # Make a kernel with the exactly correct basis functions.
111 # Useful for debugging
112 #
113 basisKernelList = []
114 for sigma in (sigma1, sigma2):
115 basisKernel = afwMath.AnalyticKernel(self.ksize, self.ksize,
116 afwMath.GaussianFunction2D(sigma, sigma))
117 basisImage = afwImage.ImageD(basisKernel.getDimensions())
118 basisKernel.computeImage(basisImage, True)
119 basisImage /= np.sum(basisImage.getArray())
121 if sigma == sigma1:
122 basisImage0 = basisImage
123 else:
124 basisImage -= basisImage0
126 basisKernelList.append(afwMath.FixedKernel(basisImage))
128 order = 1 # 1 => up to linear
129 spFunc = afwMath.PolynomialFunction2D(order)
131 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
132 exactKernel.setSpatialParameters([[1.0, 0, 0],
133 [0.0, 0.5*1e-2, 0.2e-2]])
135 rand = afwMath.Random() # make these tests repeatable by setting seed
137 addNoise = True
139 if addNoise:
140 im = self.mi.getImage()
141 afwMath.randomGaussianImage(im, rand) # N(0, 1)
142 im *= sd # N(0, sd^2)
143 del im
145 xarr, yarr = [], []
147 for x, y in [(20, 20), (60, 20),
148 (30, 35),
149 (50, 50),
150 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
151 (50, 120), (70, 80),
152 (60, 210), (20, 210),
153 ]:
154 xarr.append(x)
155 yarr.append(y)
157 for x, y in zip(xarr, yarr):
158 dx = rand.uniform() - 0.5 # random (centered) offsets
159 dy = rand.uniform() - 0.5
161 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ...
162 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b"
164 # flux = 80000 - 20*x - 10*(y/float(height))**2
165 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
166 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
167 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
168 if iy < 0 or iy >= self.mi.getHeight():
169 continue
171 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
172 if ix < 0 or ix >= self.mi.getWidth():
173 continue
175 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
176 Isample = rand.poisson(II) if addNoise else II
177 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
178 self.mi.variance[ix, iy, afwImage.LOCAL] += II
180 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
181 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
183 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED")
185 self.catalog = self.measure(self.footprintSet, self.exposure)
187 for source in self.catalog:
188 cand = measAlg.makePsfCandidate(source, self.exposure)
189 self.cellSet.insertCandidate(cand)
191 def tearDown(self):
192 del self.cellSet
193 del self.exposure
194 del self.mi
195 del self.footprintSet
196 del self.catalog
197 del self.schema
198 del self.measureSources
200 def setupDeterminer(self, exposure):
201 """Setup the starSelector and psfDeterminer"""
202 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
203 starSelectorConfig = starSelectorClass.ConfigClass()
204 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
205 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge",
206 "base_PixelFlags_flag_interpolatedCenter",
207 "base_PixelFlags_flag_saturatedCenter",
208 "base_PixelFlags_flag_crCenter",
209 ]
210 starSelectorConfig.widthStdAllowed = 0.5 # Set to match when the tolerance of the test was set
212 self.starSelector = starSelectorClass(config=starSelectorConfig)
214 self.makePsfCandidates = measAlg.MakePsfCandidatesTask()
216 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"]
217 psfDeterminerConfig = psfDeterminerClass.ConfigClass()
218 width, height = exposure.getMaskedImage().getDimensions()
219 psfDeterminerConfig.sizeCellX = width
220 psfDeterminerConfig.sizeCellY = height//3
221 psfDeterminerConfig.spatialOrder = 1
223 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig)
225 def subtractStars(self, exposure, catalog, chi_lim=-1):
226 """Subtract the exposure's PSF from all the sources in catalog"""
227 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
229 subtracted = mi.Factory(mi, True)
230 for s in catalog:
231 xc, yc = s.getX(), s.getY()
232 bbox = subtracted.getBBox(afwImage.PARENT)
233 if bbox.contains(geom.PointI(int(xc), int(yc))):
234 measAlg.subtractPsf(psf, subtracted, xc, yc)
235 chi = subtracted.Factory(subtracted, True)
236 var = subtracted.getVariance()
237 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
238 chi /= var
240 if display:
241 afwDisplay.Display(frame=1).mtv(subtracted, title="Subtracted")
242 afwDisplay.Display(frame=2).mtv(chi, title="Chi")
243 xc, yc = exposure.getWidth()//2, exposure.getHeight()//2
244 afwDisplay.Display(frame=3).mtv(psf.computeImage(geom.Point2D(xc, yc)),
245 title="Psf %.1f,%.1f" % (xc, yc))
247 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray())
249 if chi_lim > 0:
250 self.assertGreater(chi_min, -chi_lim)
251 self.assertLess(chi_max, chi_lim)
253 def testPsfexDeterminer(self):
254 """Test the (Psfex) psfDeterminer on subImages"""
256 self.setupDeterminer(self.exposure)
257 metadata = dafBase.PropertyList()
259 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
260 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
261 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
262 self.exposure.setPsf(psf)
264 # Test how well we can subtract the PSF model
265 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6)
267 # Test PsfexPsf.computeBBox
268 pos = psf.getAveragePosition()
269 self.assertEqual(psf.computeBBox(pos), psf.computeKernelImage(pos).getBBox())
270 self.assertEqual(psf.computeBBox(pos), psf.getKernel(pos).getBBox())
273class TestMemory(lsst.utils.tests.MemoryTestCase):
274 pass
277def setup_module(module):
278 lsst.utils.tests.init()
281if __name__ == "__main__": 281 ↛ 282line 281 didn't jump to line 282, because the condition on line 281 was never true
282 lsst.utils.tests.init()
283 unittest.main()