Coverage for tests/test_PsfexPsf.py: 16%
167 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-13 02:43 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-13 02:43 -0700
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 # NOTE: Warning to those trying to add sources near the edges here:
148 # self.subtractStars() assumes that every source is able to have the
149 # psf subtracted. That's not possible for sources on the edge, so the
150 # chi2 calculation that is asserted on will be off.
151 for x, y in [(20, 20), (60, 20),
152 (30, 35),
153 (50, 50),
154 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30),
155 (50, 120), (70, 80),
156 (60, 210), (20, 210),
157 ]:
158 xarr.append(x)
159 yarr.append(y)
161 for x, y in zip(xarr, yarr):
162 dx = rand.uniform() - 0.5 # random (centered) offsets
163 dy = rand.uniform() - 0.5
165 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ...
166 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b"
168 # flux = 80000 - 20*x - 10*(y/float(height))**2
169 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5))
170 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2))
171 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1):
172 if iy < 0 or iy >= self.mi.getHeight():
173 continue
175 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1):
176 if ix < 0 or ix >= self.mi.getWidth():
177 continue
179 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
180 Isample = rand.poisson(II) if addNoise else II
181 self.mi.image[ix, iy, afwImage.LOCAL] += Isample
182 self.mi.variance[ix, iy, afwImage.LOCAL] += II
184 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
185 self.cellSet = afwMath.SpatialCellSet(bbox, 100)
187 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED")
188 self.catalog = self.measure(self.footprintSet, self.exposure)
190 for source in self.catalog:
191 cand = measAlg.makePsfCandidate(source, self.exposure)
192 self.cellSet.insertCandidate(cand)
194 def tearDown(self):
195 del self.cellSet
196 del self.exposure
197 del self.mi
198 del self.footprintSet
199 del self.catalog
200 del self.schema
201 del self.measureSources
203 def setupDeterminer(self, exposure):
204 """Setup the starSelector and psfDeterminer"""
205 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"]
206 starSelectorConfig = starSelectorClass.ConfigClass()
207 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux"
208 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge",
209 "base_PixelFlags_flag_interpolatedCenter",
210 "base_PixelFlags_flag_saturatedCenter",
211 "base_PixelFlags_flag_crCenter",
212 ]
213 starSelectorConfig.widthStdAllowed = 0.5 # Set to match when the tolerance of the test was set
215 self.starSelector = starSelectorClass(config=starSelectorConfig)
217 self.makePsfCandidates = measAlg.MakePsfCandidatesTask()
219 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"]
220 psfDeterminerConfig = psfDeterminerClass.ConfigClass()
221 width, height = exposure.getMaskedImage().getDimensions()
222 psfDeterminerConfig.sizeCellX = width
223 psfDeterminerConfig.sizeCellY = height//3
224 psfDeterminerConfig.spatialOrder = 1
226 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig)
228 def subtractStars(self, exposure, catalog, chi_lim=-1):
229 """Subtract the exposure's PSF from all the sources in catalog"""
230 mi, psf = exposure.getMaskedImage(), exposure.getPsf()
232 subtracted = mi.Factory(mi, True)
233 for s in catalog:
234 xc, yc = s.getX(), s.getY()
235 bbox = subtracted.getBBox(afwImage.PARENT)
236 if bbox.contains(geom.PointI(int(xc), int(yc))):
237 measAlg.subtractPsf(psf, subtracted, xc, yc)
238 chi = subtracted.Factory(subtracted, True)
239 var = subtracted.getVariance()
240 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt
241 chi /= var
243 if display:
244 afwDisplay.Display(frame=1).mtv(subtracted, title="Subtracted")
245 afwDisplay.Display(frame=2).mtv(chi, title="Chi")
246 xc, yc = exposure.getWidth()//2, exposure.getHeight()//2
247 afwDisplay.Display(frame=3).mtv(psf.computeImage(geom.Point2D(xc, yc)),
248 title="Psf %.1f,%.1f" % (xc, yc))
250 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray())
252 if chi_lim > 0:
253 self.assertGreater(chi_min, -chi_lim)
254 self.assertLess(chi_max, chi_lim)
256 def testPsfexDeterminer(self):
257 """Test the (Psfex) psfDeterminer on subImages"""
259 self.setupDeterminer(self.exposure)
260 metadata = dafBase.PropertyList()
262 stars = self.starSelector.run(self.catalog, exposure=self.exposure)
263 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates
264 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata)
265 self.exposure.setPsf(psf)
267 # Test how well we can subtract the PSF model
268 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6)
270 # Test PsfexPsf.computeBBox
271 pos = psf.getAveragePosition()
272 self.assertEqual(psf.computeBBox(pos), psf.computeKernelImage(pos).getBBox())
273 self.assertEqual(psf.computeBBox(pos), psf.getKernel(pos).getBBox())
276class TestMemory(lsst.utils.tests.MemoryTestCase):
277 pass
280def setup_module(module):
281 lsst.utils.tests.init()
284if __name__ == "__main__": 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true
285 lsst.utils.tests.init()
286 unittest.main()