Coverage for tests/test_MeasureSources.py: 12%
212 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-10-02 08:15 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-10-02 08:15 +0000
1# This file is part of meas_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
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 GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import math
23import unittest
25import numpy as np
27import lsst.pex.exceptions
28import lsst.daf.base as dafBase
29import lsst.geom
30import lsst.afw.detection as afwDetection
31import lsst.afw.math as afwMath
32import lsst.afw.geom as afwGeom
33import lsst.afw.table as afwTable
34import lsst.afw.image as afwImage
35import lsst.meas.base as measBase
36import lsst.utils.tests
38try:
39 type(display)
40except NameError:
41 display = False
43FwhmPerSigma = 2*math.sqrt(2*math.log(2)) # FWHM for an N(0, 1) Gaussian
46def makePluginAndCat(alg, name, control, metadata=False, centroid=None):
47 schema = afwTable.SourceTable.makeMinimalSchema()
48 if centroid:
49 schema.addField(centroid + "_x", type=np.float64)
50 schema.addField(centroid + "_y", type=np.float64)
51 schema.addField(centroid + "_flag", type='Flag')
52 schema.getAliasMap().set("slot_Centroid", centroid)
53 if metadata:
54 plugin = alg(control, name, schema, dafBase.PropertySet())
55 else:
56 plugin = alg(control, name, schema)
57 cat = afwTable.SourceCatalog(schema)
58 return plugin, cat
61class MeasureSourcesTestCase(lsst.utils.tests.TestCase):
63 def setUp(self):
64 pass
66 def tearDown(self):
67 pass
69 def testCircularApertureMeasure(self):
70 mi = afwImage.MaskedImageF(lsst.geom.ExtentI(100, 200))
71 mi.set(10)
72 #
73 # Create our measuring engine
74 #
76 radii = (1.0, 5.0, 10.0) # radii to use
78 control = measBase.ApertureFluxControl()
79 control.radii = radii
81 exp = afwImage.makeExposure(mi)
82 x0, y0 = 1234, 5678
83 exp.setXY0(lsst.geom.Point2I(x0, y0))
85 plugin, cat = makePluginAndCat(measBase.CircularApertureFluxAlgorithm,
86 "test", control, True, centroid="centroid")
87 source = cat.makeRecord()
88 source.set("centroid_x", 30+x0)
89 source.set("centroid_y", 50+y0)
90 plugin.measure(source, exp)
92 for r in radii:
93 currentFlux = source.get("%s_instFlux" %
94 measBase.CircularApertureFluxAlgorithm.makeFieldPrefix("test", r))
95 self.assertAlmostEqual(10.0*math.pi*r*r/currentFlux, 1.0, places=4)
97 def testPeakLikelihoodFlux(self):
98 """Test measurement with PeakLikelihoodFlux.
100 Notes
101 -----
102 This test makes and measures a series of exposures containing just one
103 star, approximately centered.
104 """
106 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(100, 101))
107 kernelWidth = 35
108 var = 100
109 fwhm = 3.0
110 sigma = fwhm/FwhmPerSigma
111 convolutionControl = afwMath.ConvolutionControl()
112 psf = afwDetection.GaussianPsf(kernelWidth, kernelWidth, sigma)
113 psfKernel = psf.getLocalKernel(psf.getAveragePosition())
114 psfImage = psf.computeKernelImage(psf.getAveragePosition())
115 sumPsfSq = np.sum(psfImage.array**2)
116 psfSqArr = psfImage.array**2
118 for instFlux in (1000, 10000):
119 ctrInd = lsst.geom.Point2I(50, 51)
120 ctrPos = lsst.geom.Point2D(ctrInd)
122 kernelBBox = psfImage.getBBox()
123 kernelBBox.shift(lsst.geom.Extent2I(ctrInd))
125 # compute predicted instFlux error
126 unshMImage = makeFakeImage(bbox, [ctrPos], [instFlux], fwhm, var)
128 # filter image by PSF
129 unshFiltMImage = afwImage.MaskedImageF(unshMImage.getBBox())
130 afwMath.convolve(unshFiltMImage, unshMImage, psfKernel, convolutionControl)
132 # compute predicted instFlux = value of image at peak / sum(PSF^2)
133 # this is a sanity check of the algorithm, as much as anything
134 predFlux = unshFiltMImage.image[ctrInd, afwImage.LOCAL] / sumPsfSq
135 self.assertLess(abs(instFlux - predFlux), instFlux * 0.01)
137 # compute predicted instFlux error based on filtered pixels
138 # = sqrt(value of filtered variance at peak / sum(PSF^2)^2)
139 predFluxErr = math.sqrt(unshFiltMImage.variance[ctrInd, afwImage.LOCAL]) / sumPsfSq
141 # compute predicted instFlux error based on unfiltered pixels
142 # = sqrt(sum(unfiltered variance * PSF^2)) / sum(PSF^2)
143 # and compare to that derived from filtered pixels;
144 # again, this is a test of the algorithm
145 varView = afwImage.ImageF(unshMImage.variance, kernelBBox)
146 varArr = varView.array
147 unfiltPredFluxErr = math.sqrt(np.sum(varArr*psfSqArr)) / sumPsfSq
148 self.assertLess(abs(unfiltPredFluxErr - predFluxErr), predFluxErr * 0.01)
150 for fracOffset in (lsst.geom.Extent2D(0, 0), lsst.geom.Extent2D(0.2, -0.3)):
151 adjCenter = ctrPos + fracOffset
152 if fracOffset == lsst.geom.Extent2D(0, 0):
153 maskedImage = unshMImage
154 filteredImage = unshFiltMImage
155 else:
156 maskedImage = makeFakeImage(bbox, [adjCenter], [instFlux], fwhm, var)
157 # filter image by PSF
158 filteredImage = afwImage.MaskedImageF(maskedImage.getBBox())
159 afwMath.convolve(filteredImage, maskedImage, psfKernel, convolutionControl)
161 exp = afwImage.makeExposure(filteredImage)
162 exp.setPsf(psf)
163 control = measBase.PeakLikelihoodFluxControl()
164 plugin, cat = makePluginAndCat(measBase.PeakLikelihoodFluxAlgorithm, "test",
165 control, centroid="centroid")
166 source = cat.makeRecord()
167 source.set("centroid_x", adjCenter.getX())
168 source.set("centroid_y", adjCenter.getY())
169 plugin.measure(source, exp)
170 measFlux = source.get("test_instFlux")
171 measFluxErr = source.get("test_instFluxErr")
172 self.assertLess(abs(measFlux - instFlux), instFlux * 0.003)
174 self.assertLess(abs(measFluxErr - predFluxErr), predFluxErr * 0.2)
176 # try nearby points and verify that the instFlux is smaller;
177 # this checks that the sub-pixel shift is performed in the
178 # correct direction
179 for dx in (-0.2, 0, 0.2):
180 for dy in (-0.2, 0, 0.2):
181 if dx == dy == 0:
182 continue
183 offsetCtr = lsst.geom.Point2D(adjCenter[0] + dx, adjCenter[1] + dy)
184 source = cat.makeRecord()
185 source.set("centroid_x", offsetCtr.getX())
186 source.set("centroid_y", offsetCtr.getY())
187 plugin.measure(source, exp)
188 self.assertLess(source.get("test_instFlux"), measFlux)
190 # source so near edge of image that PSF does not overlap exposure
191 # should result in failure
192 for edgePos in (
193 (1, 50),
194 (50, 1),
195 (50, bbox.getHeight() - 1),
196 (bbox.getWidth() - 1, 50),
197 ):
198 source = cat.makeRecord()
199 source.set("centroid_x", edgePos[0])
200 source.set("centroid_y", edgePos[1])
201 with self.assertRaises(lsst.pex.exceptions.RangeError):
202 plugin.measure(source, exp)
204 # no PSF should result in failure: flags set
205 noPsfExposure = afwImage.ExposureF(filteredImage)
206 source = cat.makeRecord()
207 source.set("centroid_x", edgePos[0])
208 source.set("centroid_y", edgePos[1])
209 with self.assertRaises(lsst.pex.exceptions.InvalidParameterError):
210 plugin.measure(source, noPsfExposure)
212 def testPixelFlags(self):
213 width, height = 100, 100
214 mi = afwImage.MaskedImageF(width, height)
215 exp = afwImage.makeExposure(mi)
216 mi.image.set(0)
217 mask = mi.mask
218 sat = mask.getPlaneBitMask('SAT')
219 interp = mask.getPlaneBitMask('INTRP')
220 edge = mask.getPlaneBitMask('EDGE')
221 bad = mask.getPlaneBitMask('BAD')
222 nodata = mask.getPlaneBitMask('NO_DATA')
223 mask.addMaskPlane('CLIPPED')
224 clipped = mask.getPlaneBitMask('CLIPPED')
225 mask.set(0)
226 mask[20, 20, afwImage.LOCAL] = sat
227 mask[60, 60, afwImage.LOCAL] = interp
228 mask[40, 20, afwImage.LOCAL] = bad
229 mask[20, 80, afwImage.LOCAL] = nodata
230 mask[30, 30, afwImage.LOCAL] = clipped
231 mask.Factory(mask, lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(3, height))).set(edge)
232 x0, y0 = 1234, 5678
233 exp.setXY0(lsst.geom.Point2I(x0, y0))
234 control = measBase.PixelFlagsControl()
235 # Change the configuration of control to test for clipped mask
236 control.masksFpAnywhere = ['CLIPPED']
237 plugin, cat = makePluginAndCat(measBase.PixelFlagsAlgorithm, "test", control, centroid="centroid")
238 allFlags = [
239 "",
240 "edge",
241 "interpolated",
242 "interpolatedCenter",
243 "saturated",
244 "saturatedCenter",
245 "cr",
246 "crCenter",
247 "bad",
248 "clipped",
249 ]
250 for x, y, setFlags in [(1, 50, ['edge']),
251 (40, 20, ['bad']),
252 (20, 20, ['saturatedCenter',
253 'saturated']),
254 (20, 22, ['saturated']),
255 (60, 60, ['interpolatedCenter',
256 'interpolated']),
257 (60, 62, ['interpolated']),
258 (20, 80, ['edge']),
259 (30, 30, ['clipped']),
260 ]:
261 spans = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0,
262 y + y0)
263 foot = afwDetection.Footprint(spans)
264 source = cat.makeRecord()
265 source.setFootprint(foot)
266 source.set("centroid_x", x+x0)
267 source.set("centroid_y", y+y0)
268 plugin.measure(source, exp)
269 for flag in allFlags[1:]:
270 value = source.get("test_flag_" + flag)
271 if flag in setFlags:
272 self.assertTrue(value, "Flag %s should be set for %f,%f" % (flag, x, y))
273 else:
274 self.assertFalse(value, "Flag %s should not be set for %f,%f" % (flag, x, y))
276 # the new code which grabs the center of a record throws when a NaN is
277 # set in the centroid slot and the algorithm attempts to get the
278 # default center position
279 source = cat.makeRecord()
280 source.set("centroid_x", float("NAN"))
281 source.set("centroid_y", 40)
282 source.set("centroid_flag", True)
283 tmpSpanSet = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0,
284 y + y0)
285 source.setFootprint(afwDetection.Footprint(tmpSpanSet))
286 with self.assertRaises(lsst.pex.exceptions.RuntimeError):
287 plugin.measure(source, exp)
289 # Test that if there is no center and centroider that the object
290 # should look at the footprint
291 plugin, cat = makePluginAndCat(measBase.PixelFlagsAlgorithm, "test", control)
292 # The first test should raise exception because there is no footprint
293 source = cat.makeRecord()
294 with self.assertRaises(lsst.pex.exceptions.RuntimeError):
295 plugin.measure(source, exp)
296 # The second test will raise an error because no peaks are present
297 tmpSpanSet2 = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0,
298 y + y0)
299 source.setFootprint(afwDetection.Footprint(tmpSpanSet2))
300 with self.assertRaises(lsst.pex.exceptions.RuntimeError):
301 plugin.measure(source, exp)
302 # The final test should pass because it detects a peak, we are reusing
303 # the location of the clipped bit in the mask plane, so we will check
304 # first that it is False, then True
305 source.getFootprint().addPeak(x+x0, y+y0, 100)
306 self.assertFalse(source.get("test_flag_clipped"), "The clipped flag should be set False")
307 plugin.measure(source, exp)
308 self.assertTrue(source.get("test_flag_clipped"), "The clipped flag should be set True")
311def addStar(image, center, instFlux, fwhm):
312 """Add a perfect single Gaussian star to an image.
314 Parameters
315 ----------
316 image : `lsst.afw.image.ImageF`
317 Image to which the star will be added.
318 center : `list` or `tuple` of `float`, length 2
319 Position of the center of the star on the image.
320 instFlux : `float`
321 instFlux of the Gaussian star, in counts.
322 fwhm : `float`
323 FWHM of the Gaussian star, in pixels.
325 Notes
326 -----
327 Uses Python to iterate over all pixels (because there is no C++
328 function that computes a Gaussian offset by a non-integral amount).
329 """
330 sigma = fwhm/FwhmPerSigma
331 func = afwMath.GaussianFunction2D(sigma, sigma, 0)
332 starImage = afwImage.ImageF(image.getBBox())
333 # The instFlux in the region of the image will not be exactly the desired
334 # instFlux because the Gaussian does not extend to infinity, so keep track
335 # of the actual instFlux and correct for it
336 actFlux = 0
337 # No function exists that has a fractional x and y offset, so set the
338 # image the slow way
339 for i in range(image.getWidth()):
340 x = center[0] - i
341 for j in range(image.getHeight()):
342 y = center[1] - j
343 pixVal = instFlux * func(x, y)
344 actFlux += pixVal
345 starImage[i, j, afwImage.LOCAL] += pixVal
346 starImage *= instFlux / actFlux
348 image += starImage
351def makeFakeImage(bbox, centerList, instFluxList, fwhm, var):
352 """Make a fake image containing a set of stars with variance = image + var.
354 Paramters
355 ---------
356 bbox : `lsst.afw.image.Box2I`
357 Bounding box for image.
358 centerList : iterable of pairs of `float`
359 list of positions of center of star on image.
360 instFluxList : `list` of `float`
361 instFlux of each star, in counts.
362 fwhm : `float`
363 FWHM of Gaussian star, in pixels.
364 var : `float`
365 Value of variance plane, in counts.
367 Returns
368 -------
369 maskedImage : `lsst.afw.image.MaskedImageF`
370 Resulting fake image.
372 Notes
373 -----
374 It is trivial to add Poisson noise, which would be more accurate, but
375 hard to make a unit test that can reliably determine whether such an
376 image passes a test.
377 """
378 if len(centerList) != len(instFluxList):
379 raise RuntimeError("len(centerList) != len(instFluxList)")
380 maskedImage = afwImage.MaskedImageF(bbox)
381 image = maskedImage.image
382 for center, instFlux in zip(centerList, instFluxList):
383 addStar(image, center=center, instFlux=instFlux, fwhm=fwhm)
384 variance = maskedImage.variance
385 variance[:] = image
386 variance += var
387 return maskedImage
390class TestMemory(lsst.utils.tests.MemoryTestCase):
391 pass
394def setup_module(module):
395 lsst.utils.tests.init()
398if __name__ == "__main__": 398 ↛ 399line 398 didn't jump to line 399, because the condition on line 398 was never true
399 lsst.utils.tests.init()
400 unittest.main()