Coverage for tests/test_hsm.py: 17%
370 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-13 03:21 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-13 03:21 -0700
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, varyBBox=False, wrongBBox=False):
338 afwDetection.Psf.__init__(self, isFixed=not varyBBox)
339 self.dimensions = geom.Extent2I(width, height)
340 self.sigma = sigma
341 self.varyBBox = varyBBox # To address DM-29863
342 self.wrongBBox = wrongBBox # To address DM-30426
344 def _doComputeKernelImage(self, position=None, color=None):
345 bbox = self.computeBBox(position, color)
346 img = afwImage.Image(bbox, dtype=np.float64)
347 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1]
348 rsqr = x**2 + y**2
349 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2)
350 img.array /= np.sum(img.array)
351 return img
353 def _doComputeImage(self, position=None, color=None):
354 bbox = self.computeBBox(position, color)
355 if self.wrongBBox:
356 # For DM-30426:
357 # Purposely make computeImage.getBBox() and computeBBox()
358 # inconsistent. Old shapeHSM code attempted to infer the former
359 # from the latter, but was unreliable. New code infers the former
360 # directly, so this inconsistency no longer breaks things.
361 bbox.shift(geom.Extent2I(1, 1))
362 img = afwImage.Image(bbox, dtype=np.float64)
363 y, x = np.ogrid[float(bbox.minY):bbox.maxY+1, bbox.minX:bbox.maxX+1]
364 x -= (position.x - np.floor(position.x+0.5))
365 y -= (position.y - np.floor(position.y+0.5))
366 rsqr = x**2 + y**2
367 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2)
368 img.array /= np.sum(img.array)
369 img.setXY0(geom.Point2I(
370 img.getX0() + np.floor(position.x+0.5),
371 img.getY0() + np.floor(position.y+0.5)
372 ))
373 return img
375 def _doComputeBBox(self, position=None, color=None):
376 # Variable size bbox for addressing DM-29863
377 dims = self.dimensions
378 if self.varyBBox:
379 if position.x > 20.0:
380 dims = dims + geom.Extent2I(2, 2)
381 return geom.Box2I(geom.Point2I(-dims/2), dims)
383 def _doComputeShape(self, position=None, color=None):
384 return afwGeom.ellipses.Quadrupole(self.sigma**2, self.sigma**2, 0.0)
387class PsfMomentsTestCase(unittest.TestCase):
388 """A test case for shape measurement"""
390 @lsst.utils.tests.methodParameters(
391 # Make Cartesian product of settings to feed to methodParameters
392 **dict(list(zip(
393 (kwargs := dict(
394 width=(2.0, 3.0, 4.0),
395 useSourceCentroidOffset=(True, False),
396 varyBBox=(True, False),
397 wrongBBox=(True, False),
398 center=(
399 (23.0, 34.0), # various offsets that might cause trouble
400 (23.5, 34.0),
401 (23.5, 34.5),
402 (23.15, 34.25),
403 (22.81, 34.01),
404 (22.81, 33.99),
405 (1.2, 1.3), # psfImage extends outside exposure; that's okay
406 (-100.0, -100.0),
407 (-100.5, -100.0),
408 (-100.5, -100.5),
409 )
410 )).keys(),
411 zip(*itertools.product(*kwargs.values()))
412 )))
413 )
414 def testHsmPsfMoments(
415 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center
416 ):
417 psf = PyGaussianPsf(
418 35, 35, width,
419 varyBBox=varyBBox,
420 wrongBBox=wrongBBox
421 )
422 exposure = afwImage.ExposureF(45, 56)
423 exposure.getMaskedImage().set(1.0, 0, 1.0)
424 exposure.setPsf(psf)
426 # perform the shape measurement
427 msConfig = base.SingleFrameMeasurementConfig()
428 msConfig.algorithms.names = ["ext_shapeHSM_HsmPsfMoments"]
429 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsControl()
430 self.assertFalse(control.useSourceCentroidOffset)
431 control.useSourceCentroidOffset = useSourceCentroidOffset
432 plugin, cat = makePluginAndCat(
433 lsst.meas.extensions.shapeHSM.HsmPsfMomentsAlgorithm,
434 "ext_shapeHSM_HsmPsfMoments", centroid="centroid",
435 control=control)
436 source = cat.addNew()
437 source.set("centroid_x", center[0])
438 source.set("centroid_y", center[1])
439 offset = geom.Point2I(*center)
440 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
441 source.setFootprint(afwDetection.Footprint(tmpSpans))
442 plugin.measure(source, exposure)
443 x = source.get("ext_shapeHSM_HsmPsfMoments_x")
444 y = source.get("ext_shapeHSM_HsmPsfMoments_y")
445 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx")
446 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy")
447 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy")
448 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag"))
449 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_no_pixels"))
450 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_not_contained"))
451 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_parent_source"))
453 self.assertAlmostEqual(x, 0.0, 3)
454 self.assertAlmostEqual(y, 0.0, 3)
456 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
457 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS)
458 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS)
459 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS)
461 @lsst.utils.tests.methodParameters(
462 # Make Cartesian product of settings to feed to methodParameters
463 **dict(list(zip(
464 (kwargs := dict(
465 width=(2.0, 3.0, 4.0),
466 useSourceCentroidOffset=(True, False),
467 varyBBox=(True, False),
468 wrongBBox=(True, False),
469 center=(
470 (23.0, 34.0), # various offsets that might cause trouble
471 (23.5, 34.0),
472 (23.5, 34.5),
473 (23.15, 34.25),
474 (22.81, 34.01),
475 (22.81, 33.99),
476 )
477 )).keys(),
478 zip(*itertools.product(*kwargs.values()))
479 )))
480 )
481 def testHsmPsfMomentsDebiased(
482 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center
483 ):
484 # As a note, it's really hard to actually unit test whether we've
485 # succesfully "debiased" these measurements. That would require a
486 # many-object comparison of moments with and without noise. So we just
487 # test similar to the biased moments above.
488 var = 1.2
489 # As we reduce the flux, our deviation from the expected value
490 # increases, so decrease tolerance.
491 for flux, decimals in [
492 (1e6, 3),
493 (1e4, 1),
494 (1e3, 0),
495 ]:
496 psf = PyGaussianPsf(
497 35, 35, width,
498 varyBBox=varyBBox,
499 wrongBBox=wrongBBox
500 )
501 exposure = afwImage.ExposureF(45, 56)
502 exposure.getMaskedImage().set(1.0, 0, var)
503 exposure.setPsf(psf)
505 # perform the shape measurement
506 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
507 self.assertTrue(control.useSourceCentroidOffset)
508 self.assertEqual(control.noiseSource, "variance")
509 control.useSourceCentroidOffset = useSourceCentroidOffset
510 plugin, cat = makePluginAndCat(
511 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
512 "ext_shapeHSM_HsmPsfMomentsDebiased",
513 centroid="centroid",
514 psfflux="base_PsfFlux",
515 control=control
516 )
517 source = cat.addNew()
518 source.set("centroid_x", center[0])
519 source.set("centroid_y", center[1])
520 offset = geom.Point2I(*center)
521 source.set("base_PsfFlux_instFlux", flux)
522 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
523 source.setFootprint(afwDetection.Footprint(tmpSpans))
525 plugin.measure(source, exposure)
526 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
527 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
528 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
529 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
530 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
531 for flag in [
532 "ext_shapeHSM_HsmPsfMomentsDebiased_flag",
533 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels",
534 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained",
535 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source",
536 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"
537 ]:
538 self.assertFalse(source.get(flag))
540 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
542 self.assertAlmostEqual(x, 0.0, decimals)
543 self.assertAlmostEqual(y, 0.0, decimals)
545 T = expected.getIxx() + expected.getIyy()
546 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals)
547 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals)
548 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals)
550 # Repeat using noiseSource='meta'. Should get nearly the same
551 # results if BGMEAN is set to `var` above.
552 exposure2 = afwImage.ExposureF(45, 56)
553 # set the variance plane to something else to ensure we're
554 # ignoring it
555 exposure2.getMaskedImage().set(1.0, 0, 2*var+1.1)
556 exposure2.setPsf(psf)
557 exposure2.getMetadata().set("BGMEAN", var)
559 control2 = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
560 control2.noiseSource = "meta"
561 control2.useSourceCentroidOffset = useSourceCentroidOffset
562 plugin2, cat2 = makePluginAndCat(
563 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
564 "ext_shapeHSM_HsmPsfMomentsDebiased",
565 centroid="centroid",
566 psfflux="base_PsfFlux",
567 control=control2
568 )
569 source2 = cat2.addNew()
570 source2.set("centroid_x", center[0])
571 source2.set("centroid_y", center[1])
572 offset2 = geom.Point2I(*center)
573 source2.set("base_PsfFlux_instFlux", flux)
574 tmpSpans2 = afwGeom.SpanSet.fromShape(int(width), offset=offset2)
575 source2.setFootprint(afwDetection.Footprint(tmpSpans2))
577 plugin2.measure(source2, exposure2)
578 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
579 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
580 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
581 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
582 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
583 for flag in [
584 "ext_shapeHSM_HsmPsfMomentsDebiased_flag",
585 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels",
586 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained",
587 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source",
588 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"
589 ]:
590 self.assertFalse(source.get(flag))
592 # Would be identically equal, but variance input via "BGMEAN" is
593 # consumed in c++ as a double, where variance from the variance
594 # plane is a c++ float.
595 self.assertAlmostEqual(x, x2, 8)
596 self.assertAlmostEqual(y, y2, 8)
597 self.assertAlmostEqual(xx, xx2, 5)
598 self.assertAlmostEqual(xy, xy2, 5)
599 self.assertAlmostEqual(yy, yy2, 5)
601 testHsmPsfMomentsDebiasedEdgeArgs = dict(
602 width=(2.0, 3.0, 4.0),
603 useSourceCentroidOffset=(True, False),
604 center=(
605 (1.2, 1.3),
606 (33.2, 50.1)
607 )
608 )
610 @lsst.utils.tests.methodParameters(
611 # Make Cartesian product of settings to feed to methodParameters
612 **dict(list(zip(
613 (kwargs := dict(
614 width=(2.0, 3.0, 4.0),
615 useSourceCentroidOffset=(True, False),
616 center=[
617 (1.2, 1.3),
618 (33.2, 50.1)
619 ]
620 )).keys(),
621 zip(*itertools.product(*kwargs.values()))
622 )))
623 )
624 def testHsmPsfMomentsDebiasedEdge(self, width, useSourceCentroidOffset, center):
625 # As we reduce the flux, our deviation from the expected value
626 # increases, so decrease tolerance.
627 var = 1.2
628 for flux, decimals in [
629 (1e6, 3),
630 (1e4, 2),
631 (1e3, 1),
632 ]:
633 psf = PyGaussianPsf(35, 35, width)
634 exposure = afwImage.ExposureF(45, 56)
635 exposure.getMaskedImage().set(1.0, 0, 2*var+1.1)
636 exposure.setPsf(psf)
638 # perform the shape measurement
639 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
640 control.useSourceCentroidOffset = useSourceCentroidOffset
641 self.assertEqual(control.noiseSource, "variance")
642 plugin, cat = makePluginAndCat(
643 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
644 "ext_shapeHSM_HsmPsfMomentsDebiased",
645 centroid="centroid",
646 psfflux="base_PsfFlux",
647 control=control
648 )
649 source = cat.addNew()
650 source.set("centroid_x", center[0])
651 source.set("centroid_y", center[1])
652 offset = geom.Point2I(*center)
653 source.set("base_PsfFlux_instFlux", flux)
654 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
655 source.setFootprint(afwDetection.Footprint(tmpSpans))
657 # Edge fails when setting noise from var plane
658 with self.assertRaises(base.MeasurementError):
659 plugin.measure(source, exposure)
661 # Succeeds when noise is from meta
662 exposure.getMetadata().set("BGMEAN", var)
663 control.noiseSource = "meta"
664 plugin, cat = makePluginAndCat(
665 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
666 "ext_shapeHSM_HsmPsfMomentsDebiased",
667 centroid="centroid",
668 psfflux="base_PsfFlux",
669 control=control
670 )
671 source = cat.addNew()
672 source.set("centroid_x", center[0])
673 source.set("centroid_y", center[1])
674 offset = geom.Point2I(*center)
675 source.set("base_PsfFlux_instFlux", flux)
676 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
677 source.setFootprint(afwDetection.Footprint(tmpSpans))
678 plugin.measure(source, exposure)
680 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x")
681 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y")
682 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx")
683 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy")
684 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy")
685 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag"))
686 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels"))
687 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained"))
688 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source"))
689 # but _does_ set EDGE flag in this case
690 self.assertTrue(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge"))
692 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0))
694 self.assertAlmostEqual(x, 0.0, decimals)
695 self.assertAlmostEqual(y, 0.0, decimals)
697 T = expected.getIxx() + expected.getIyy()
698 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals)
699 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals)
700 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals)
702 # But fails hard if meta doesn't contain BGMEAN
703 exposure.getMetadata().remove("BGMEAN")
704 plugin, cat = makePluginAndCat(
705 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
706 "ext_shapeHSM_HsmPsfMomentsDebiased",
707 centroid="centroid",
708 psfflux="base_PsfFlux",
709 control=control
710 )
711 source = cat.addNew()
712 source.set("centroid_x", center[0])
713 source.set("centroid_y", center[1])
714 offset = geom.Point2I(*center)
715 source.set("base_PsfFlux_instFlux", flux)
716 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset)
717 source.setFootprint(afwDetection.Footprint(tmpSpans))
718 with self.assertRaises(base.FatalAlgorithmError):
719 plugin.measure(source, exposure)
721 def testHsmPsfMomentsDebiasedBadNoiseSource(self):
722 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl()
723 control.noiseSource = "ACM"
724 with self.assertRaises(base.MeasurementError):
725 makePluginAndCat(
726 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm,
727 "ext_shapeHSM_HsmPsfMomentsDebiased",
728 centroid="centroid",
729 control=control
730 )
733class TestMemory(lsst.utils.tests.MemoryTestCase):
734 pass
737def setup_module(module):
738 lsst.utils.tests.init()
741if __name__ == "__main__": 741 ↛ 742line 741 didn't jump to line 742, because the condition on line 741 was never true
742 lsst.utils.tests.init()
743 unittest.main()