Coverage for tests/test_hsm.py : 22%

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):
134 print("Making plugin ", alg, name)
135 if control is None:
136 control = alg.ConfigClass()
137 schema = afwTable.SourceTable.makeMinimalSchema()
138 if centroid:
139 schema.addField(centroid + "_x", type=float)
140 schema.addField(centroid + "_y", type=float)
141 schema.addField(centroid + "_flag", type='Flag')
142 schema.getAliasMap().set("slot_Centroid", centroid)
143 if metadata:
144 plugin = alg(control, name, schema, PropertySet())
145 else:
146 plugin = alg(control, name, schema)
147 cat = afwTable.SourceCatalog(schema)
148 if centroid:
149 cat.defineCentroid(centroid)
150 return plugin, cat
153class ShapeTestCase(unittest.TestCase):
154 """A test case for shape measurement"""
156 def setUp(self):
158 # load the known values
159 self.dataDir = os.path.join(os.getenv('MEAS_EXTENSIONS_SHAPEHSM_DIR'), "tests", "data")
160 self.bkgd = 1000.0 # standard for atlas image
161 self.offset = geom.Extent2I(1234, 1234)
162 self.xy0 = geom.Point2I(5678, 9876)
164 def tearDown(self):
165 del self.offset
166 del self.xy0
168 def runMeasurement(self, algorithmName, imageid, x, y, v):
169 """Run the measurement algorithm on an image"""
170 # load the test image
171 imgFile = os.path.join(self.dataDir, "image.%d.fits" % imageid)
172 img = afwImage.ImageF(imgFile)
173 img -= self.bkgd
174 nx, ny = img.getWidth(), img.getHeight()
175 msk = afwImage.Mask(geom.Extent2I(nx, ny), 0x0)
176 var = afwImage.ImageF(geom.Extent2I(nx, ny), v)
177 mimg = afwImage.MaskedImageF(img, msk, var)
178 msk.getArray()[:] = np.where(np.fabs(img.getArray()) < 1.0e-8, msk.getPlaneBitMask("BAD"), 0)
180 # Put it in a bigger image, in case it matters
181 big = afwImage.MaskedImageF(self.offset + mimg.getDimensions())
182 big.getImage().set(0)
183 big.getMask().set(0)
184 big.getVariance().set(v)
185 subBig = afwImage.MaskedImageF(big, geom.Box2I(big.getXY0() + self.offset, mimg.getDimensions()))
186 subBig.assign(mimg)
187 mimg = big
188 mimg.setXY0(self.xy0)
190 exposure = afwImage.makeExposure(mimg)
191 cdMatrix = np.array([1.0/(2.53*3600.0), 0.0, 0.0, 1.0/(2.53*3600.0)])
192 cdMatrix.shape = (2, 2)
193 exposure.setWcs(afwGeom.makeSkyWcs(crpix=geom.Point2D(1.0, 1.0),
194 crval=geom.SpherePoint(0, 0, geom.degrees),
195 cdMatrix=cdMatrix))
197 # load the corresponding test psf
198 psfFile = os.path.join(self.dataDir, "psf.%d.fits" % imageid)
199 psfImg = afwImage.ImageD(psfFile)
200 psfImg -= self.bkgd
202 kernel = afwMath.FixedKernel(psfImg)
203 kernelPsf = algorithms.KernelPsf(kernel)
204 exposure.setPsf(kernelPsf)
206 # perform the shape measurement
207 msConfig = base.SingleFrameMeasurementConfig()
208 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass.AlgClass
209 control = base.SingleFramePlugin.registry[algorithmName].PluginClass.ConfigClass().makeControl()
210 msConfig.algorithms.names = [algorithmName]
211 # Note: It is essential to remove the floating point part of the position for the
212 # Algorithm._apply. Otherwise, when the PSF is realised it will have been warped
213 # to account for the sub-pixel offset and we won't get *exactly* this PSF.
214 plugin, table = makePluginAndCat(alg, algorithmName, control, centroid="centroid")
215 center = geom.Point2D(int(x), int(y)) + geom.Extent2D(self.offset + geom.Extent2I(self.xy0))
216 source = table.makeRecord()
217 source.set("centroid_x", center.getX())
218 source.set("centroid_y", center.getY())
219 source.setFootprint(afwDetection.Footprint(afwGeom.SpanSet(exposure.getBBox(afwImage.PARENT))))
220 plugin.measure(source, exposure)
222 return source
224 def testHsmShape(self):
225 """Test that we can instantiate and play with a measureShape"""
227 nFail = 0
228 msg = ""
230 for (algNum, algName), (i, imageid) in itertools.product(enumerate(correction_methods),
231 enumerate(file_indices)):
232 algorithmName = "ext_shapeHSM_HsmShape" + algName[0:1].upper() + algName[1:].lower()
234 source = self.runMeasurement(algorithmName, imageid, x_centroid[i], y_centroid[i], sky_var[i])
236 ##########################################
237 # see how we did
238 if algName in ("KSB"):
239 # Need to convert g1,g2 --> e1,e2 because GalSim has done that
240 # for the expected values ("for consistency")
241 g1 = source.get(algorithmName + "_g1")
242 g2 = source.get(algorithmName + "_g2")
243 scale = 2.0/(1.0 + g1**2 + g2**2)
244 e1 = g1*scale
245 e2 = g2*scale
246 sigma = source.get(algorithmName + "_sigma")
247 else:
248 e1 = source.get(algorithmName + "_e1")
249 e2 = source.get(algorithmName + "_e2")
250 sigma = 0.5*source.get(algorithmName + "_sigma")
251 resolution = source.get(algorithmName + "_resolution")
252 flags = source.get(algorithmName + "_flag")
254 tests = [
255 # label known-value measured tolerance
256 ["e1", float(e1_expected[i][algNum]), e1, 0.5*10**-SHAPE_DECIMALS],
257 ["e2", float(e2_expected[i][algNum]), e2, 0.5*10**-SHAPE_DECIMALS],
258 ["resolution", float(resolution_expected[i][algNum]), resolution, 0.5*10**-SIZE_DECIMALS],
260 # sigma won't match exactly because
261 # we're using skyvar=mean(var) instead of measured value ... expected a difference
262 ["sigma", float(sigma_e_expected[i][algNum]), sigma, 0.07],
263 ["shapeStatus", 0, flags, 0],
264 ]
266 for test in tests:
267 label, know, hsm, limit = test
268 err = hsm - know
269 msgTmp = "%-12s %s %5s: %6.6f %6.6f (val-known) = %.3g\n" % (algName, imageid,
270 label, know, hsm, err)
271 if not np.isfinite(err) or abs(err) > limit:
272 msg += msgTmp
273 nFail += 1
275 self.assertAlmostEqual(g1 if algName in ("KSB") else e1, galsim_e1[i][algNum], SHAPE_DECIMALS)
276 self.assertAlmostEqual(g2 if algName in ("KSB") else e2, galsim_e2[i][algNum], SHAPE_DECIMALS)
277 self.assertAlmostEqual(resolution, galsim_resolution[i][algNum], SIZE_DECIMALS)
278 self.assertAlmostEqual(sigma, galsim_err[i][algNum], delta=0.07)
280 self.assertEqual(nFail, 0, "\n"+msg)
282 def testHsmSourceMoments(self):
283 for (i, imageid) in enumerate(file_indices):
284 source = self.runMeasurement("ext_shapeHSM_HsmSourceMoments", imageid,
285 x_centroid[i], y_centroid[i], sky_var[i])
286 x = source.get("ext_shapeHSM_HsmSourceMoments_x")
287 y = source.get("ext_shapeHSM_HsmSourceMoments_y")
288 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx")
289 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy")
290 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy")
292 # Centroids from GalSim use the FITS lower-left corner of 1,1
293 offset = self.xy0 + self.offset
294 self.assertAlmostEqual(x - offset.getX(), centroid_expected[i][0] - 1, 3)
295 self.assertAlmostEqual(y - offset.getY(), centroid_expected[i][1] - 1, 3)
297 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius(
298 moments_expected[i][1], moments_expected[i][2], moments_expected[i][0]))
300 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
301 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
302 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
304 def testHsmSourceMomentsRound(self):
305 for (i, imageid) in enumerate(file_indices):
306 source = self.runMeasurement("ext_shapeHSM_HsmSourceMomentsRound", imageid,
307 x_centroid[i], y_centroid[i], sky_var[i])
308 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x")
309 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y")
310 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx")
311 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy")
312 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy")
313 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux")
315 # Centroids from GalSim use the FITS lower-left corner of 1,1
316 offset = self.xy0 + self.offset
317 self.assertAlmostEqual(x - offset.getX(), round_moments_expected[i][4] - 1, 3)
318 self.assertAlmostEqual(y - offset.getY(), round_moments_expected[i][5] - 1, 3)
320 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius(
321 round_moments_expected[i][1], round_moments_expected[i][2], round_moments_expected[i][0]))
322 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
323 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
324 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
326 self.assertAlmostEqual(flux, round_moments_expected[i][3], SHAPE_DECIMALS)
328 def testHsmPsfMoments(self):
329 for width in (2.0, 3.0, 4.0):
330 psf = afwDetection.GaussianPsf(35, 35, width)
331 exposure = afwImage.ExposureF(45, 56)
332 exposure.getMaskedImage().set(1.0, 0, 1.0)
333 exposure.setPsf(psf)
335 # perform the shape measurement
336 msConfig = base.SingleFrameMeasurementConfig()
337 msConfig.algorithms.names = ["ext_shapeHSM_HsmPsfMoments"]
338 plugin, cat = makePluginAndCat(lsst.meas.extensions.shapeHSM.HsmPsfMomentsAlgorithm,
339 "ext_shapeHSM_HsmPsfMoments", centroid="centroid",
340 control=lsst.meas.extensions.shapeHSM.HsmPsfMomentsControl())
341 source = cat.addNew()
342 source.set("centroid_x", 23)
343 source.set("centroid_y", 34)
344 offset = geom.Point2I(23, 34)
345 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
346 source.setFootprint(afwDetection.Footprint(tmpSpans))
347 plugin.measure(source, exposure)
348 x = source.get("ext_shapeHSM_HsmPsfMoments_x")
349 y = source.get("ext_shapeHSM_HsmPsfMoments_y")
350 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx")
351 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy")
352 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy")
354 self.assertAlmostEqual(x, 0.0, 3)
355 self.assertAlmostEqual(y, 0.0, 3)
357 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
359 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
360 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
361 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
364class TestMemory(lsst.utils.tests.MemoryTestCase):
365 pass
368def setup_module(module):
369 lsst.utils.tests.init()
372if __name__ == "__main__": 372 ↛ 373line 372 didn't jump to line 373, because the condition on line 372 was never true
373 lsst.utils.tests.init()
374 unittest.main()