Coverage for tests/test_hsm.py : 14%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2#
3# LSST Data Management System
4#
5# Copyright 2008-2016 AURA/LSST.
6#
7# This product includes software developed by the
8# LSST Project (http://www.lsst.org/).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the LSST License Statement and
21# the GNU General Public License along with this program. If not,
22# see <https://www.lsstcorp.org/LegalNotices/>.
23#
24import os
25import numpy as np
26import unittest
27import itertools
29import lsst.afw.image as afwImage
30import lsst.afw.math as afwMath
31from lsst.daf.base import PropertySet
32import lsst.meas.base as base
33import lsst.meas.algorithms as algorithms
34import lsst.afw.detection as afwDetection
35import lsst.afw.table as afwTable
36import lsst.afw.geom as afwGeom
37import lsst.geom as geom
38import lsst.afw.geom.ellipses as afwEll
39import lsst.utils.tests
40import lsst.meas.extensions.shapeHSM
42SIZE_DECIMALS = 2 # Number of decimals for equality in sizes
43SHAPE_DECIMALS = 3 # Number of decimals for equality in shapes
45# The following values are pulled directly from GalSim's test_hsm.py:
46file_indices = [0, 2, 4, 6, 8]
47x_centroid = [35.888, 19.44, 8.74, 20.193, 57.94]
48y_centroid = [19.845, 25.047, 11.92, 38.93, 27.73]
49sky_var = [35.01188, 35.93418, 35.15456, 35.11146, 35.16454]
50correction_methods = ["KSB", "BJ", "LINEAR", "REGAUSS"]
51# Note: expected results give shear for KSB and distortion for others, but the results below have
52# converted KSB expected results to distortion for the sake of consistency
53e1_expected = np.array([
54 [0.467603106752, 0.381211727, 0.398856937, 0.401755571],
55 [0.28618443944, 0.199222784, 0.233883543, 0.234257525],
56 [0.271533794146, 0.158049396, 0.183517068, 0.184893412],
57 [-0.293754156071, -0.457024541, 0.123946584, -0.609233462],
58 [0.557720893779, 0.374143023, 0.714147448, 0.435404409]])
59e2_expected = np.array([
60 [-0.867225166489, -0.734855778, -0.777027588, -0.774684891],
61 [-0.469354341577, -0.395520479, -0.502540961, -0.464466257],
62 [-0.519775291311, -0.471589061, -0.574750641, -0.529664935],
63 [0.345688365839, -0.342047099, 0.120603755, -0.44609129428863525],
64 [0.525728304099, 0.370691830, 0.702724807, 0.433999442]])
65resolution_expected = np.array([
66 [0.796144249, 0.835624917, 0.835624917, 0.827796187],
67 [0.685023735, 0.699602704, 0.699602704, 0.659457638],
68 [0.634736458, 0.651040481, 0.651040481, 0.614663396],
69 [0.477027015, 0.477210752, 0.477210752, 0.423157447],
70 [0.595205998, 0.611824797, 0.611824797, 0.563582092]])
71sigma_e_expected = np.array([
72 [0.016924826, 0.014637648, 0.014637648, 0.014465546],
73 [0.075769504, 0.073602324, 0.073602324, 0.064414520],
74 [0.110253112, 0.106222900, 0.106222900, 0.099357106],
75 [0.185276702, 0.184300955, 0.184300955, 0.173478300],
76 [0.073020065, 0.070270966, 0.070270966, 0.061856263]])
77# End of GalSim's values
79# These values calculated using GalSim's HSM as part of GalSim
80galsim_e1 = np.array([
81 [0.399292618036, 0.381213068962, 0.398856908083, 0.401749581099],
82 [0.155929282308, 0.199228107929, 0.233882278204, 0.234371587634],
83 [0.150018423796, 0.158052951097, 0.183515056968, 0.184561833739],
84 [-2.6984937191, -0.457033962011, 0.123932465911, -0.60886412859],
85 [0.33959621191, 0.374140143394, 0.713756918907, 0.43560180068],
86])
87galsim_e2 = np.array([
88 [-0.74053555727, -0.734855830669, -0.777024209499, -0.774700462818],
89 [-0.25573053956, -0.395517915487, -0.50251352787, -0.464388132095],
90 [-0.287168383598, -0.471584022045, -0.574719130993, -0.5296921134],
91 [3.1754450798, -0.342054128647, 0.120592080057, -0.446093201637],
92 [0.320115834475, 0.370669454336, 0.702303349972, 0.433968126774],
93])
94galsim_resolution = np.array([
95 [0.79614430666, 0.835625052452, 0.835625052452, 0.827822327614],
96 [0.685023903847, 0.699601829052, 0.699601829052, 0.659438848495],
97 [0.634736537933, 0.651039719582, 0.651039719582, 0.614759743214],
98 [0.477026551962, 0.47721144557, 0.47721144557, 0.423227936029],
99 [0.595205545425, 0.611821532249, 0.611821532249, 0.563564240932],
100])
101galsim_err = np.array([
102 [0.0169247947633, 0.0146376201883, 0.0146376201883, 0.0144661813974],
103 [0.0757696777582, 0.0736026018858, 0.0736026018858, 0.0644160583615],
104 [0.110252402723, 0.106222368777, 0.106222368777, 0.0993555411696],
105 [0.185278102756, 0.184301897883, 0.184301897883, 0.17346136272],
106 [0.0730196461082, 0.0702708885074, 0.0702708885074, 0.0618583671749],
107])
109moments_expected = np.array([ # sigma, e1, e2
110 [2.24490427971, 0.336240686301, -0.627372910656],
111 [1.9031778574, 0.150566105384, -0.245272792302],
112 [1.77790760994, 0.112286123389, -0.286203939641],
113 [1.45464873314, -0.155597168978, -0.102008266223],
114 [1.63144648075, 0.22886961923, 0.228813588897],
115])
116centroid_expected = np.array([ # x, y
117 [36.218247328, 20.5678722157],
118 [20.325744838, 25.4176650386],
119 [9.54257706283, 12.6134786199],
120 [20.6407850048, 39.5864802706],
121 [58.5008586442, 28.2850942049],
122])
124round_moments_expected = np.array([ # sigma, e1, e2, flux, x, y
125 [2.40270376205, 0.197810277343, -0.372329413891, 3740.22436523, 36.4032272633, 20.4847916447],
126 [1.89714717865, 0.046496052295, -0.0987404286861, 776.709594727, 20.2893584046, 25.4230368047],
127 [1.77995181084, 0.0416346564889, -0.143147706985, 534.59197998, 9.51994111869, 12.6250775205],
128 [1.46549296379, -0.0831127092242, -0.0628845766187, 348.294403076, 20.6242279632, 39.5941625731],
129 [1.64031589031, 0.0867517963052, 0.0940798297524, 793.374450684, 58.4728765002, 28.2686937854],
130])
133def makePluginAndCat(alg, name, control=None, metadata=False, centroid=None, psfflux=None):
134 print("Making plugin ", alg, name)
135 if control is None:
136 control = alg.ConfigClass()
137 schema = afwTable.SourceTable.makeMinimalSchema()
138 if centroid:
139 lsst.afw.table.Point2DKey.addFields(
140 schema, centroid, "centroid", "pixel"
141 )
142 schema.getAliasMap().set("slot_Centroid", centroid)
143 if psfflux:
144 base.PsfFluxAlgorithm(base.PsfFluxControl(), psfflux, schema)
145 schema.getAliasMap().set("slot_PsfFlux", psfflux)
146 if metadata:
147 plugin = alg(control, name, schema, PropertySet())
148 else:
149 plugin = alg(control, name, schema)
150 cat = afwTable.SourceCatalog(schema)
151 if centroid:
152 cat.defineCentroid(centroid)
153 return plugin, cat
156class ShapeTestCase(unittest.TestCase):
157 """A test case for shape measurement"""
159 def setUp(self):
161 # load the known values
162 self.dataDir = os.path.join(os.getenv('MEAS_EXTENSIONS_SHAPEHSM_DIR'), "tests", "data")
163 self.bkgd = 1000.0 # standard for atlas image
164 self.offset = geom.Extent2I(1234, 1234)
165 self.xy0 = geom.Point2I(5678, 9876)
167 def tearDown(self):
168 del self.offset
169 del self.xy0
171 def runMeasurement(self, algorithmName, imageid, x, y, v):
172 """Run the measurement algorithm on an image"""
173 # load the test image
174 imgFile = os.path.join(self.dataDir, "image.%d.fits" % imageid)
175 img = afwImage.ImageF(imgFile)
176 img -= self.bkgd
177 nx, ny = img.getWidth(), img.getHeight()
178 msk = afwImage.Mask(geom.Extent2I(nx, ny), 0x0)
179 var = afwImage.ImageF(geom.Extent2I(nx, ny), v)
180 mimg = afwImage.MaskedImageF(img, msk, var)
181 msk.getArray()[:] = np.where(np.fabs(img.getArray()) < 1.0e-8, msk.getPlaneBitMask("BAD"), 0)
183 # Put it in a bigger image, in case it matters
184 big = afwImage.MaskedImageF(self.offset + mimg.getDimensions())
185 big.getImage().set(0)
186 big.getMask().set(0)
187 big.getVariance().set(v)
188 subBig = afwImage.MaskedImageF(big, geom.Box2I(big.getXY0() + self.offset, mimg.getDimensions()))
189 subBig.assign(mimg)
190 mimg = big
191 mimg.setXY0(self.xy0)
193 exposure = afwImage.makeExposure(mimg)
194 cdMatrix = np.array([1.0/(2.53*3600.0), 0.0, 0.0, 1.0/(2.53*3600.0)])
195 cdMatrix.shape = (2, 2)
196 exposure.setWcs(afwGeom.makeSkyWcs(crpix=geom.Point2D(1.0, 1.0),
197 crval=geom.SpherePoint(0, 0, geom.degrees),
198 cdMatrix=cdMatrix))
200 # load the corresponding test psf
201 psfFile = os.path.join(self.dataDir, "psf.%d.fits" % imageid)
202 psfImg = afwImage.ImageD(psfFile)
203 psfImg -= self.bkgd
205 kernel = afwMath.FixedKernel(psfImg)
206 kernelPsf = algorithms.KernelPsf(kernel)
207 exposure.setPsf(kernelPsf)
209 # perform the shape measurement
210 msConfig = base.SingleFrameMeasurementConfig()
211 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass.AlgClass
212 control = base.SingleFramePlugin.registry[algorithmName].PluginClass.ConfigClass().makeControl()
213 msConfig.algorithms.names = [algorithmName]
214 # Note: It is essential to remove the floating point part of the position for the
215 # Algorithm._apply. Otherwise, when the PSF is realised it will have been warped
216 # to account for the sub-pixel offset and we won't get *exactly* this PSF.
217 plugin, table = makePluginAndCat(alg, algorithmName, control, centroid="centroid")
218 center = geom.Point2D(int(x), int(y)) + geom.Extent2D(self.offset + geom.Extent2I(self.xy0))
219 source = table.makeRecord()
220 source.set("centroid_x", center.getX())
221 source.set("centroid_y", center.getY())
222 source.setFootprint(afwDetection.Footprint(afwGeom.SpanSet(exposure.getBBox(afwImage.PARENT))))
223 plugin.measure(source, exposure)
225 return source
227 def testHsmShape(self):
228 """Test that we can instantiate and play with a measureShape"""
230 nFail = 0
231 msg = ""
233 for (algNum, algName), (i, imageid) in itertools.product(enumerate(correction_methods),
234 enumerate(file_indices)):
235 algorithmName = "ext_shapeHSM_HsmShape" + algName[0:1].upper() + algName[1:].lower()
237 source = self.runMeasurement(algorithmName, imageid, x_centroid[i], y_centroid[i], sky_var[i])
239 ##########################################
240 # see how we did
241 if algName in ("KSB"):
242 # Need to convert g1,g2 --> e1,e2 because GalSim has done that
243 # for the expected values ("for consistency")
244 g1 = source.get(algorithmName + "_g1")
245 g2 = source.get(algorithmName + "_g2")
246 scale = 2.0/(1.0 + g1**2 + g2**2)
247 e1 = g1*scale
248 e2 = g2*scale
249 sigma = source.get(algorithmName + "_sigma")
250 else:
251 e1 = source.get(algorithmName + "_e1")
252 e2 = source.get(algorithmName + "_e2")
253 sigma = 0.5*source.get(algorithmName + "_sigma")
254 resolution = source.get(algorithmName + "_resolution")
255 flags = source.get(algorithmName + "_flag")
257 tests = [
258 # label known-value measured tolerance
259 ["e1", float(e1_expected[i][algNum]), e1, 0.5*10**-SHAPE_DECIMALS],
260 ["e2", float(e2_expected[i][algNum]), e2, 0.5*10**-SHAPE_DECIMALS],
261 ["resolution", float(resolution_expected[i][algNum]), resolution, 0.5*10**-SIZE_DECIMALS],
263 # sigma won't match exactly because
264 # we're using skyvar=mean(var) instead of measured value ... expected a difference
265 ["sigma", float(sigma_e_expected[i][algNum]), sigma, 0.07],
266 ["shapeStatus", 0, flags, 0],
267 ]
269 for test in tests:
270 label, know, hsm, limit = test
271 err = hsm - know
272 msgTmp = "%-12s %s %5s: %6.6f %6.6f (val-known) = %.3g\n" % (algName, imageid,
273 label, know, hsm, err)
274 if not np.isfinite(err) or abs(err) > limit:
275 msg += msgTmp
276 nFail += 1
278 self.assertAlmostEqual(g1 if algName in ("KSB") else e1, galsim_e1[i][algNum], SHAPE_DECIMALS)
279 self.assertAlmostEqual(g2 if algName in ("KSB") else e2, galsim_e2[i][algNum], SHAPE_DECIMALS)
280 self.assertAlmostEqual(resolution, galsim_resolution[i][algNum], SIZE_DECIMALS)
281 self.assertAlmostEqual(sigma, galsim_err[i][algNum], delta=0.07)
283 self.assertEqual(nFail, 0, "\n"+msg)
285 def testHsmSourceMoments(self):
286 for (i, imageid) in enumerate(file_indices):
287 source = self.runMeasurement("ext_shapeHSM_HsmSourceMoments", imageid,
288 x_centroid[i], y_centroid[i], sky_var[i])
289 x = source.get("ext_shapeHSM_HsmSourceMoments_x")
290 y = source.get("ext_shapeHSM_HsmSourceMoments_y")
291 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx")
292 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy")
293 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy")
295 # Centroids from GalSim use the FITS lower-left corner of 1,1
296 offset = self.xy0 + self.offset
297 self.assertAlmostEqual(x - offset.getX(), centroid_expected[i][0] - 1, 3)
298 self.assertAlmostEqual(y - offset.getY(), centroid_expected[i][1] - 1, 3)
300 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius(
301 moments_expected[i][1], moments_expected[i][2], moments_expected[i][0]))
303 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
304 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
305 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
307 def testHsmSourceMomentsRound(self):
308 for (i, imageid) in enumerate(file_indices):
309 source = self.runMeasurement("ext_shapeHSM_HsmSourceMomentsRound", imageid,
310 x_centroid[i], y_centroid[i], sky_var[i])
311 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x")
312 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y")
313 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx")
314 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy")
315 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy")
316 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux")
318 # Centroids from GalSim use the FITS lower-left corner of 1,1
319 offset = self.xy0 + self.offset
320 self.assertAlmostEqual(x - offset.getX(), round_moments_expected[i][4] - 1, 3)
321 self.assertAlmostEqual(y - offset.getY(), round_moments_expected[i][5] - 1, 3)
323 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius(
324 round_moments_expected[i][1], round_moments_expected[i][2], round_moments_expected[i][0]))
325 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
326 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
327 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
329 self.assertAlmostEqual(flux, round_moments_expected[i][3], SHAPE_DECIMALS)
332class PyGaussianPsf(afwDetection.Psf):
333 # Like afwDetection.GaussianPsf, but handles computeImage exactly instead of
334 # via interpolation. This is a subminimal implementation. It works for the
335 # tests here but isn't fully functional as a Psf class.
337 def __init__(self, width, height, sigma):
338 afwDetection.Psf.__init__(self, isFixed=True)
339 self.dimensions = geom.Extent2I(width, height)
340 self.sigma = sigma
342 def _doComputeKernelImage(self, position=None, color=None):
343 bbox = self.computeBBox()
344 img = afwImage.Image(bbox, dtype=np.float64)
345 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1]
346 rsqr = x**2 + y**2
347 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2)
348 img.array /= np.sum(img.array)
349 return img
351 def _doComputeImage(self, position=None, color=None):
352 bbox = self.computeBBox()
353 img = afwImage.Image(bbox, dtype=np.float64)
354 y, x = np.ogrid[float(bbox.minY):bbox.maxY+1, bbox.minX:bbox.maxX+1]
355 x -= (position.x - np.floor(position.x+0.5))
356 y -= (position.y - np.floor(position.y+0.5))
357 rsqr = x**2 + y**2
358 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2)
359 img.array /= np.sum(img.array)
360 img.setXY0(geom.Point2I(
361 img.getX0() + np.floor(position.x+0.5),
362 img.getY0() + np.floor(position.y+0.5)
363 ))
364 return img
366 def _doComputeBBox(self, position=None, color=None):
367 return geom.Box2I(geom.Point2I(-self.dimensions/2), self.dimensions)
369 def _doComputeShape(self, position=None, color=None):
370 return afwGeom.ellipses.Quadrupole(self.sigma**2, self.sigma**2, 0.0)
373class PsfMomentsTestCase(unittest.TestCase):
374 """A test case for shape measurement"""
376 def testHsmPsfMoments(self):
377 for width in (2.0, 3.0, 4.0):
378 for useSourceCentroidOffset in [True, False]:
379 for center in [
380 (23.0, 34.0), # various offsets that might cause trouble
381 (23.5, 34.0),
382 (23.5, 34.5),
383 (23.15, 34.25),
384 (22.81, 34.01),
385 (22.81, 33.99),
386 (1.2, 1.3), # psfImage extends outside exposure; that's okay
387 ]:
388 psf = PyGaussianPsf(35, 35, width)
389 exposure = afwImage.ExposureF(45, 56)
390 exposure.getMaskedImage().set(1.0, 0, 1.0)
391 exposure.setPsf(psf)
393 # perform the shape measurement
394 msConfig = base.SingleFrameMeasurementConfig()
395 msConfig.algorithms.names = ["ext_shapeHSM_HsmPsfMoments"]
396 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsControl()
397 self.assertFalse(control.useSourceCentroidOffset)
398 control.useSourceCentroidOffset = useSourceCentroidOffset
399 plugin, cat = makePluginAndCat(
400 lsst.meas.extensions.shapeHSM.HsmPsfMomentsAlgorithm,
401 "ext_shapeHSM_HsmPsfMoments", centroid="centroid",
402 control=control)
403 source = cat.addNew()
404 source.set("centroid_x", center[0])
405 source.set("centroid_y", center[1])
406 offset = geom.Point2I(*center)
407 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
408 source.setFootprint(afwDetection.Footprint(tmpSpans))
409 plugin.measure(source, exposure)
410 x = source.get("ext_shapeHSM_HsmPsfMoments_x")
411 y = source.get("ext_shapeHSM_HsmPsfMoments_y")
412 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx")
413 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy")
414 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy")
415 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag"))
416 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_no_pixels"))
417 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_not_contained"))
418 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_parent_source"))
420 self.assertAlmostEqual(x, 0.0, 3)
421 self.assertAlmostEqual(y, 0.0, 3)
423 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
424 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
425 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
426 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
428 def testHsmPsfMomentsDebiased(self):
429 # As a note, it's really hard to actually unit test whether we've
430 # succesfully "debiased" these measurements. That would require a
431 # many-object comparison of moments with and without noise. So we just
432 # test similar to the biased moments above.
433 var = 1.2
434 for width in (2.0, 3.0, 4.0):
435 for useSourceCentroidOffset in [True, False]:
436 for center in [
437 (23.0, 34.0), # various offsets that might cause trouble
438 (23.5, 34.0),
439 (23.5, 34.5),
440 (23.15, 34.25),
441 (22.81, 34.01),
442 (22.81, 33.99)
443 ]:
444 # As we reduce the flux, our deviation from the expected value
445 # increases, so decrease tolerance.
446 for flux, decimals in [
447 (1e6, 3),
448 (1e4, 2),
449 (1e3, 1),
450 ]:
451 psf = PyGaussianPsf(35, 35, width)
452 exposure = afwImage.ExposureF(45, 56)
453 exposure.getMaskedImage().set(1.0, 0, var)
454 exposure.setPsf(psf)
456 # perform the shape measurement
457 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
458 self.assertTrue(control.useSourceCentroidOffset)
459 self.assertEqual(control.noiseSource, "variance")
460 control.useSourceCentroidOffset = useSourceCentroidOffset
461 plugin, cat = makePluginAndCat(
462 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
463 "ext_shapeHSM_HsmPsfMomentsDebiased",
464 centroid="centroid",
465 psfflux="base_PsfFlux",
466 control=control
467 )
468 source = cat.addNew()
469 source.set("centroid_x", center[0])
470 source.set("centroid_y", center[1])
471 offset = geom.Point2I(*center)
472 source.set("base_PsfFlux_instFlux", flux)
473 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
474 source.setFootprint(afwDetection.Footprint(tmpSpans))
476 plugin.measure(source, exposure)
477 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
478 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
479 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
480 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
481 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
482 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag"))
483 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels"))
484 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained"))
485 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source"))
486 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"))
488 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
490 self.assertAlmostEqual(x, 0.0, decimals)
491 self.assertAlmostEqual(y, 0.0, decimals)
493 T = expected.getIxx() + expected.getIyy()
494 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals)
495 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals)
496 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals)
498 # Repeat using noiseSource='meta'. Should get nearly the same
499 # results if BGMEAN is set to `var` above.
500 exposure2 = afwImage.ExposureF(45, 56)
501 # set the variance plane to something else to ensure we're
502 # ignoring it
503 exposure2.getMaskedImage().set(1.0, 0, 2*var+1.1)
504 exposure2.setPsf(psf)
505 exposure2.getMetadata().set("BGMEAN", var)
507 control2 = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
508 control2.noiseSource = "meta"
509 control2.useSourceCentroidOffset = useSourceCentroidOffset
510 plugin2, cat2 = makePluginAndCat(
511 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
512 "ext_shapeHSM_HsmPsfMomentsDebiased",
513 centroid="centroid",
514 psfflux="base_PsfFlux",
515 control=control2
516 )
517 source2 = cat2.addNew()
518 source2.set("centroid_x", center[0])
519 source2.set("centroid_y", center[1])
520 offset2 = geom.Point2I(*center)
521 source2.set("base_PsfFlux_instFlux", flux)
522 tmpSpans2 = afwGeom.SpanSet.fromShape(int(width), offset=offset2)
523 source2.setFootprint(afwDetection.Footprint(tmpSpans2))
525 plugin2.measure(source2, exposure2)
526 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
527 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
528 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
529 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
530 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
531 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag"))
532 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels"))
533 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained"))
534 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source"))
535 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"))
537 # Would be identically equal, but variance input via "BGMEAN" is
538 # consumed in c++ as a double, where variance from the variance
539 # plane is a c++ float.
540 self.assertAlmostEqual(x, x2, 8)
541 self.assertAlmostEqual(y, y2, 8)
542 self.assertAlmostEqual(xx, xx2, 6)
543 self.assertAlmostEqual(xy, xy2, 6)
544 self.assertAlmostEqual(yy, yy2, 6)
546 def testHsmPsfMomentsDebiasedEdge(self):
547 var = 1.2
548 for width in (2.0, 3.0, 4.0):
549 for useSourceCentroidOffset in [True, False]:
550 for center in [
551 (1.2, 1.3),
552 (33.2, 50.1)
553 ]:
554 # As we reduce the flux, our deviation from the expected value
555 # increases, so decrease tolerance.
556 for flux, decimals in [
557 (1e6, 3),
558 (1e4, 2),
559 (1e3, 1),
560 ]:
561 psf = PyGaussianPsf(35, 35, width)
562 exposure = afwImage.ExposureF(45, 56)
563 exposure.getMaskedImage().set(1.0, 0, 2*var+1.1)
564 exposure.setPsf(psf)
566 # perform the shape measurement
567 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
568 control.useSourceCentroidOffset = useSourceCentroidOffset
569 self.assertEqual(control.noiseSource, "variance")
570 plugin, cat = makePluginAndCat(
571 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
572 "ext_shapeHSM_HsmPsfMomentsDebiased",
573 centroid="centroid",
574 psfflux="base_PsfFlux",
575 control=control
576 )
577 source = cat.addNew()
578 source.set("centroid_x", center[0])
579 source.set("centroid_y", center[1])
580 offset = geom.Point2I(*center)
581 source.set("base_PsfFlux_instFlux", flux)
582 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
583 source.setFootprint(afwDetection.Footprint(tmpSpans))
585 # Edge fails when setting noise from var plane
586 with self.assertRaises(base.MeasurementError):
587 plugin.measure(source, exposure)
589 # Succeeds when noise is from meta
590 exposure.getMetadata().set("BGMEAN", var)
591 control.noiseSource = "meta"
592 plugin, cat = makePluginAndCat(
593 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
594 "ext_shapeHSM_HsmPsfMomentsDebiased",
595 centroid="centroid",
596 psfflux="base_PsfFlux",
597 control=control
598 )
599 source = cat.addNew()
600 source.set("centroid_x", center[0])
601 source.set("centroid_y", center[1])
602 offset = geom.Point2I(*center)
603 source.set("base_PsfFlux_instFlux", flux)
604 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
605 source.setFootprint(afwDetection.Footprint(tmpSpans))
606 plugin.measure(source, exposure)
608 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
609 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
610 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
611 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
612 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
613 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag"))
614 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels"))
615 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained"))
616 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source"))
617 # but _does_ set EDGE flag in this case
618 self.assertTrue(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"))
620 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
622 self.assertAlmostEqual(x, 0.0, decimals)
623 self.assertAlmostEqual(y, 0.0, decimals)
625 T = expected.getIxx() + expected.getIyy()
626 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals)
627 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals)
628 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals)
630 # But fails hard if meta doesn't contain BGMEAN
631 exposure.getMetadata().remove("BGMEAN")
632 plugin, cat = makePluginAndCat(
633 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
634 "ext_shapeHSM_HsmPsfMomentsDebiased",
635 centroid="centroid",
636 psfflux="base_PsfFlux",
637 control=control
638 )
639 source = cat.addNew()
640 source.set("centroid_x", center[0])
641 source.set("centroid_y", center[1])
642 offset = geom.Point2I(*center)
643 source.set("base_PsfFlux_instFlux", flux)
644 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
645 source.setFootprint(afwDetection.Footprint(tmpSpans))
646 with self.assertRaises(base.FatalAlgorithmError):
647 plugin.measure(source, exposure)
649 def testHsmPsfMomentsDebiasedBadNoiseSource(self):
650 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
651 control.noiseSource = "ACM"
652 with self.assertRaises(base.MeasurementError):
653 makePluginAndCat(
654 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
655 "ext_shapeHSM_HsmPsfMomentsDebiased",
656 centroid="centroid",
657 control=control
658 )
661class TestMemory(lsst.utils.tests.MemoryTestCase):
662 pass
665def setup_module(module):
666 lsst.utils.tests.init()
669if __name__ == "__main__": 669 ↛ 670line 669 didn't jump to line 670, because the condition on line 669 was never true
670 lsst.utils.tests.init()
671 unittest.main()