Coverage for tests/test_hsm.py: 14%

485 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-23 12:01 +0000

1# This file is part of meas_extensions_shapeHSM. 

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/>. 

21 

22import itertools 

23import os 

24import unittest 

25 

26import lsst.afw.detection as afwDetection 

27import lsst.afw.geom as afwGeom 

28import lsst.afw.geom.ellipses as afwEll 

29import lsst.afw.image as afwImage 

30import lsst.afw.math as afwMath 

31import lsst.afw.table as afwTable 

32import lsst.geom as geom 

33import lsst.meas.algorithms as algorithms 

34import lsst.meas.base as base 

35import lsst.meas.base.tests 

36import lsst.meas.extensions.shapeHSM as shapeHSM 

37import lsst.utils.tests 

38import numpy as np 

39from lsst.daf.base import PropertySet 

40import lsst.pex.config as pexConfig 

41import galsim 

42 

43SIZE_DECIMALS = 2 # Number of decimals for equality in sizes 

44SHAPE_DECIMALS = 3 # Number of decimals for equality in shapes 

45 

46# The following values are pulled directly from GalSim's test_hsm.py: 

47file_indices = [0, 2, 4, 6, 8] 

48x_centroid = [35.888, 19.44, 8.74, 20.193, 57.94] 

49y_centroid = [19.845, 25.047, 11.92, 38.93, 27.73] 

50sky_var = [35.01188, 35.93418, 35.15456, 35.11146, 35.16454] 

51correction_methods = ["KSB", "BJ", "LINEAR", "REGAUSS"] 

52# Note: expected results give shear for KSB and distortion for others, but the results below have 

53# converted KSB expected results to distortion for the sake of consistency 

54e1_expected = np.array([ 

55 [0.467603106752, 0.381211727, 0.398856937, 0.401755571], 

56 [0.28618443944, 0.199222784, 0.233883543, 0.234257525], 

57 [0.271533794146, 0.158049396, 0.183517068, 0.184893412], 

58 [-0.293754156071, -0.457024541, 0.123946584, -0.609233462], 

59 [0.557720893779, 0.374143023, 0.714147448, 0.435404409]]) 

60e2_expected = np.array([ 

61 [-0.867225166489, -0.734855778, -0.777027588, -0.774684891], 

62 [-0.469354341577, -0.395520479, -0.502540961, -0.464466257], 

63 [-0.519775291311, -0.471589061, -0.574750641, -0.529664935], 

64 [0.345688365839, -0.342047099, 0.120603755, -0.44609129428863525], 

65 [0.525728304099, 0.370691830, 0.702724807, 0.433999442]]) 

66resolution_expected = np.array([ 

67 [0.796144249, 0.835624917, 0.835624917, 0.827796187], 

68 [0.685023735, 0.699602704, 0.699602704, 0.659457638], 

69 [0.634736458, 0.651040481, 0.651040481, 0.614663396], 

70 [0.477027015, 0.477210752, 0.477210752, 0.423157447], 

71 [0.595205998, 0.611824797, 0.611824797, 0.563582092]]) 

72sigma_e_expected = np.array([ 

73 [0.016924826, 0.014637648, 0.014637648, 0.014465546], 

74 [0.075769504, 0.073602324, 0.073602324, 0.064414520], 

75 [0.110253112, 0.106222900, 0.106222900, 0.099357106], 

76 [0.185276702, 0.184300955, 0.184300955, 0.173478300], 

77 [0.073020065, 0.070270966, 0.070270966, 0.061856263]]) 

78# End of GalSim's values 

79 

80# These values calculated using GalSim's HSM as part of GalSim 

81galsim_e1 = np.array([ 

82 [0.399292618036, 0.381213068962, 0.398856908083, 0.401749581099], 

83 [0.155929282308, 0.199228107929, 0.233882278204, 0.234371587634], 

84 [0.150018423796, 0.158052951097, 0.183515056968, 0.184561833739], 

85 [-2.6984937191, -0.457033962011, 0.123932465911, -0.60886412859], 

86 [0.33959621191, 0.374140143394, 0.713756918907, 0.43560180068], 

87]) 

88galsim_e2 = np.array([ 

89 [-0.74053555727, -0.734855830669, -0.777024209499, -0.774700462818], 

90 [-0.25573053956, -0.395517915487, -0.50251352787, -0.464388132095], 

91 [-0.287168383598, -0.471584022045, -0.574719130993, -0.5296921134], 

92 [3.1754450798, -0.342054128647, 0.120592080057, -0.446093201637], 

93 [0.320115834475, 0.370669454336, 0.702303349972, 0.433968126774], 

94]) 

95galsim_resolution = np.array([ 

96 [0.79614430666, 0.835625052452, 0.835625052452, 0.827822327614], 

97 [0.685023903847, 0.699601829052, 0.699601829052, 0.659438848495], 

98 [0.634736537933, 0.651039719582, 0.651039719582, 0.614759743214], 

99 [0.477026551962, 0.47721144557, 0.47721144557, 0.423227936029], 

100 [0.595205545425, 0.611821532249, 0.611821532249, 0.563564240932], 

101]) 

102galsim_err = np.array([ 

103 [0.0169247947633, 0.0146376201883, 0.0146376201883, 0.0144661813974], 

104 [0.0757696777582, 0.0736026018858, 0.0736026018858, 0.0644160583615], 

105 [0.110252402723, 0.106222368777, 0.106222368777, 0.0993555411696], 

106 [0.185278102756, 0.184301897883, 0.184301897883, 0.17346136272], 

107 [0.0730196461082, 0.0702708885074, 0.0702708885074, 0.0618583671749], 

108]) 

109 

110moments_expected = np.array([ # sigma, e1, e2 

111 [2.24490427971, 0.336240686301, -0.627372910656], 

112 [1.9031778574, 0.150566105384, -0.245272792302], 

113 [1.77790760994, 0.112286123389, -0.286203939641], 

114 [1.45464873314, -0.155597168978, -0.102008266223], 

115 [1.63144648075, 0.22886961923, 0.228813588897], 

116]) 

117centroid_expected = np.array([ # x, y 

118 [36.218247328, 20.5678722157], 

119 [20.325744838, 25.4176650386], 

120 [9.54257706283, 12.6134786199], 

121 [20.6407850048, 39.5864802706], 

122 [58.5008586442, 28.2850942049], 

123]) 

124 

125round_moments_expected = np.array([ # sigma, e1, e2, flux, x, y 

126 [2.40270376205, 0.197810277343, -0.372329413891, 3740.22436523, 36.4032272633, 20.4847916447], 

127 [1.89714717865, 0.046496052295, -0.0987404286861, 776.709594727, 20.2893584046, 25.4230368047], 

128 [1.77995181084, 0.0416346564889, -0.143147706985, 534.59197998, 9.51994111869, 12.6250775205], 

129 [1.46549296379, -0.0831127092242, -0.0628845766187, 348.294403076, 20.6242279632, 39.5941625731], 

130 [1.64031589031, 0.0867517963052, 0.0940798297524, 793.374450684, 58.4728765002, 28.2686937854], 

131]) 

132 

133 

134def makePluginAndCat(alg, name, control=None, metadata=False, centroid=None, psfflux=None, addFlux=False): 

135 print("Making plugin ", alg, name) 

136 if control is None: 

137 control = alg.ConfigClass() 

138 if addFlux: 

139 control.addFlux = True 

140 schema = afwTable.SourceTable.makeMinimalSchema() 

141 if centroid: 

142 lsst.afw.table.Point2DKey.addFields(schema, centroid, "centroid", "pixel") 

143 schema.getAliasMap().set("slot_Centroid", centroid) 

144 if psfflux: 

145 base.PsfFluxAlgorithm(base.PsfFluxControl(), psfflux, schema) 

146 schema.getAliasMap().set("slot_PsfFlux", psfflux) 

147 if metadata: 

148 plugin = alg(control, name, schema, PropertySet()) 

149 else: 

150 plugin = alg(control, name, schema) 

151 cat = afwTable.SourceCatalog(schema) 

152 if centroid: 

153 cat.defineCentroid(centroid) 

154 return plugin, cat 

155 

156 

157class MomentsTestCase(unittest.TestCase): 

158 """A test case for shape measurement""" 

159 

160 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) 

166 

167 def tearDown(self): 

168 del self.offset 

169 del self.xy0 

170 

171 def runMeasurement(self, algorithmName, imageid, x, y, v, addFlux=False): 

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) 

182 

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) 

192 

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( 

197 afwGeom.makeSkyWcs( 

198 crpix=geom.Point2D(1.0, 1.0), crval=geom.SpherePoint(0, 0, geom.degrees), cdMatrix=cdMatrix 

199 ) 

200 ) 

201 

202 # load the corresponding test psf 

203 psfFile = os.path.join(self.dataDir, "psf.%d.fits" % imageid) 

204 psfImg = afwImage.ImageD(psfFile) 

205 psfImg -= self.bkgd 

206 

207 kernel = afwMath.FixedKernel(psfImg) 

208 kernelPsf = algorithms.KernelPsf(kernel) 

209 exposure.setPsf(kernelPsf) 

210 

211 # perform the shape measurement 

212 msConfig = base.SingleFrameMeasurementConfig() 

213 msConfig.plugins.names |= [algorithmName] 

214 control = msConfig.plugins[algorithmName] 

215 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass 

216 # NOTE: It is essential to remove the floating point part of the position for the 

217 # Algorithm._apply. Otherwise, when the PSF is realised it will have been warped 

218 # to account for the sub-pixel offset and we won't get *exactly* this PSF. 

219 plugin, table = makePluginAndCat( 

220 alg, algorithmName, control, centroid="centroid", metadata=True, addFlux=addFlux 

221 ) 

222 center = geom.Point2D(int(x), int(y)) + geom.Extent2D(self.offset + geom.Extent2I(self.xy0)) 

223 source = table.makeRecord() 

224 source.set("centroid_x", center.getX()) 

225 source.set("centroid_y", center.getY()) 

226 source.setFootprint(afwDetection.Footprint(afwGeom.SpanSet(exposure.getBBox(afwImage.PARENT)))) 

227 plugin.measure(source, exposure) 

228 

229 return source 

230 

231 def testHsmSourceMoments(self): 

232 for i, imageid in enumerate(file_indices): 

233 source = self.runMeasurement( 

234 "ext_shapeHSM_HsmSourceMoments", imageid, x_centroid[i], y_centroid[i], sky_var[i] 

235 ) 

236 x = source.get("ext_shapeHSM_HsmSourceMoments_x") 

237 y = source.get("ext_shapeHSM_HsmSourceMoments_y") 

238 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx") 

239 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy") 

240 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy") 

241 

242 # Centroids from GalSim use the FITS lower-left corner of 1,1 

243 offset = self.xy0 + self.offset 

244 self.assertAlmostEqual(x - offset.getX(), centroid_expected[i][0] - 1, 3) 

245 self.assertAlmostEqual(y - offset.getY(), centroid_expected[i][1] - 1, 3) 

246 

247 expected = afwEll.Quadrupole( 

248 afwEll.SeparableDistortionDeterminantRadius( 

249 moments_expected[i][1], moments_expected[i][2], moments_expected[i][0] 

250 ) 

251 ) 

252 

253 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS) 

254 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS) 

255 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS) 

256 

257 def testHsmSourceMomentsRound(self): 

258 for i, imageid in enumerate(file_indices): 

259 source = self.runMeasurement( 

260 "ext_shapeHSM_HsmSourceMomentsRound", 

261 imageid, 

262 x_centroid[i], 

263 y_centroid[i], 

264 sky_var[i], 

265 addFlux=True, 

266 ) 

267 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x") 

268 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y") 

269 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx") 

270 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy") 

271 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy") 

272 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux") 

273 

274 # Centroids from GalSim use the FITS lower-left corner of 1,1 

275 offset = self.xy0 + self.offset 

276 self.assertAlmostEqual(x - offset.getX(), round_moments_expected[i][4] - 1, 3) 

277 self.assertAlmostEqual(y - offset.getY(), round_moments_expected[i][5] - 1, 3) 

278 

279 expected = afwEll.Quadrupole( 

280 afwEll.SeparableDistortionDeterminantRadius( 

281 round_moments_expected[i][1], round_moments_expected[i][2], round_moments_expected[i][0] 

282 ) 

283 ) 

284 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS) 

285 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS) 

286 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS) 

287 

288 self.assertAlmostEqual(flux, round_moments_expected[i][3], SHAPE_DECIMALS) 

289 

290 def testHsmSourceMomentsVsSdssShape(self): 

291 # Initialize a config and activate the plugins. 

292 sfmConfig = base.SingleFrameMeasurementConfig() 

293 sfmConfig.plugins.names |= ["ext_shapeHSM_HsmSourceMoments", "base_SdssShape"] 

294 

295 # Create a minimal schema (columns). 

296 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema() 

297 

298 # Instantiate the task. 

299 sfmTask = base.SingleFrameMeasurementTask(config=sfmConfig, schema=schema) 

300 

301 # Create a simple, test dataset. 

302 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(100, 100)) 

303 dataset = lsst.meas.base.tests.TestDataset(bbox) 

304 

305 # First source is a point. 

306 dataset.addSource(100000.0, lsst.geom.Point2D(49.5, 49.5)) 

307 

308 # Second source is a galaxy. 

309 dataset.addSource(300000.0, lsst.geom.Point2D(76.3, 79.2), afwGeom.Quadrupole(2.0, 3.0, 0.5)) 

310 

311 # Third source is also a galaxy. 

312 dataset.addSource(250000.0, lsst.geom.Point2D(28.9, 41.35), afwGeom.Quadrupole(1.8, 3.5, 0.4)) 

313 

314 # Get the exposure and catalog. 

315 exposure, catalog = dataset.realize(10.0, sfmTask.schema, randomSeed=0) 

316 

317 # Run the measurement task. 

318 sfmTask.run(catalog, exposure) 

319 cat = catalog.asAstropy() 

320 

321 # Get the moments from the catalog. 

322 xSdss, ySdss = cat["base_SdssShape_x"], cat["base_SdssShape_y"] 

323 xxSdss, xySdss, yySdss = cat["base_SdssShape_xx"], cat["base_SdssShape_xy"], cat["base_SdssShape_yy"] 

324 xHsm, yHsm = cat["ext_shapeHSM_HsmSourceMoments_x"], cat["ext_shapeHSM_HsmSourceMoments_y"] 

325 xxHsm, xyHsm, yyHsm = ( 

326 cat["ext_shapeHSM_HsmSourceMoments_xx"], 

327 cat["ext_shapeHSM_HsmSourceMoments_xy"], 

328 cat["ext_shapeHSM_HsmSourceMoments_yy"], 

329 ) 

330 

331 # Loop over the sources and check that the moments are the same. 

332 for i in range(3): 

333 self.assertAlmostEqual(xSdss[i], xHsm[i], 2) 

334 self.assertAlmostEqual(ySdss[i], yHsm[i], 2) 

335 self.assertAlmostEqual(xxSdss[i], xxHsm[i], SHAPE_DECIMALS) 

336 self.assertAlmostEqual(xySdss[i], xyHsm[i], SHAPE_DECIMALS) 

337 self.assertAlmostEqual(yySdss[i], yyHsm[i], SHAPE_DECIMALS) 

338 

339 

340class ShapeTestCase(unittest.TestCase): 

341 """A test case for shape measurement""" 

342 

343 def setUp(self): 

344 

345 # load the known values 

346 self.dataDir = os.path.join(os.getenv('MEAS_EXTENSIONS_SHAPEHSM_DIR'), "tests", "data") 

347 self.bkgd = 1000.0 # standard for atlas image 

348 self.offset = geom.Extent2I(1234, 1234) 

349 self.xy0 = geom.Point2I(5678, 9876) 

350 

351 def tearDown(self): 

352 del self.offset 

353 del self.xy0 

354 

355 def runMeasurement(self, algorithmName, imageid, x, y, v): 

356 """Run the measurement algorithm on an image""" 

357 # load the test image 

358 imgFile = os.path.join(self.dataDir, "image.%d.fits" % imageid) 

359 img = afwImage.ImageF(imgFile) 

360 img -= self.bkgd 

361 nx, ny = img.getWidth(), img.getHeight() 

362 msk = afwImage.Mask(geom.Extent2I(nx, ny), 0x0) 

363 var = afwImage.ImageF(geom.Extent2I(nx, ny), v) 

364 mimg = afwImage.MaskedImageF(img, msk, var) 

365 msk.getArray()[:] = np.where(np.fabs(img.getArray()) < 1.0e-8, msk.getPlaneBitMask("BAD"), 0) 

366 

367 # Put it in a bigger image, in case it matters 

368 big = afwImage.MaskedImageF(self.offset + mimg.getDimensions()) 

369 big.getImage().set(0) 

370 big.getMask().set(0) 

371 big.getVariance().set(v) 

372 subBig = afwImage.MaskedImageF(big, geom.Box2I(big.getXY0() + self.offset, mimg.getDimensions())) 

373 subBig.assign(mimg) 

374 mimg = big 

375 mimg.setXY0(self.xy0) 

376 

377 exposure = afwImage.makeExposure(mimg) 

378 cdMatrix = np.array([1.0/(2.53*3600.0), 0.0, 0.0, 1.0/(2.53*3600.0)]) 

379 cdMatrix.shape = (2, 2) 

380 exposure.setWcs(afwGeom.makeSkyWcs(crpix=geom.Point2D(1.0, 1.0), 

381 crval=geom.SpherePoint(0, 0, geom.degrees), 

382 cdMatrix=cdMatrix)) 

383 

384 # load the corresponding test psf 

385 psfFile = os.path.join(self.dataDir, "psf.%d.fits" % imageid) 

386 psfImg = afwImage.ImageD(psfFile) 

387 psfImg -= self.bkgd 

388 

389 kernel = afwMath.FixedKernel(psfImg) 

390 kernelPsf = algorithms.KernelPsf(kernel) 

391 exposure.setPsf(kernelPsf) 

392 

393 # perform the shape measurement 

394 msConfig = base.SingleFrameMeasurementConfig() 

395 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass.AlgClass 

396 control = base.SingleFramePlugin.registry[algorithmName].PluginClass.ConfigClass().makeControl() 

397 msConfig.algorithms.names = [algorithmName] 

398 # Note: It is essential to remove the floating point part of the position for the 

399 # Algorithm._apply. Otherwise, when the PSF is realised it will have been warped 

400 # to account for the sub-pixel offset and we won't get *exactly* this PSF. 

401 plugin, table = makePluginAndCat(alg, algorithmName, control, centroid="centroid") 

402 center = geom.Point2D(int(x), int(y)) + geom.Extent2D(self.offset + geom.Extent2I(self.xy0)) 

403 source = table.makeRecord() 

404 source.set("centroid_x", center.getX()) 

405 source.set("centroid_y", center.getY()) 

406 source.setFootprint(afwDetection.Footprint(afwGeom.SpanSet(exposure.getBBox(afwImage.PARENT)))) 

407 plugin.measure(source, exposure) 

408 

409 return source 

410 

411 def testHsmShape(self): 

412 """Test that we can instantiate and play with a measureShape""" 

413 

414 nFail = 0 

415 msg = "" 

416 

417 for (algNum, algName), (i, imageid) in itertools.product(enumerate(correction_methods), 

418 enumerate(file_indices)): 

419 algorithmName = "ext_shapeHSM_HsmShape" + algName[0:1].upper() + algName[1:].lower() 

420 

421 source = self.runMeasurement(algorithmName, imageid, x_centroid[i], y_centroid[i], sky_var[i]) 

422 

423 ########################################## 

424 # see how we did 

425 if algName in ("KSB"): 

426 # Need to convert g1,g2 --> e1,e2 because GalSim has done that 

427 # for the expected values ("for consistency") 

428 g1 = source.get(algorithmName + "_g1") 

429 g2 = source.get(algorithmName + "_g2") 

430 scale = 2.0/(1.0 + g1**2 + g2**2) 

431 e1 = g1*scale 

432 e2 = g2*scale 

433 sigma = source.get(algorithmName + "_sigma") 

434 else: 

435 e1 = source.get(algorithmName + "_e1") 

436 e2 = source.get(algorithmName + "_e2") 

437 sigma = 0.5*source.get(algorithmName + "_sigma") 

438 resolution = source.get(algorithmName + "_resolution") 

439 flags = source.get(algorithmName + "_flag") 

440 

441 tests = [ 

442 # label known-value measured tolerance 

443 ["e1", float(e1_expected[i][algNum]), e1, 0.5*10**-SHAPE_DECIMALS], 

444 ["e2", float(e2_expected[i][algNum]), e2, 0.5*10**-SHAPE_DECIMALS], 

445 ["resolution", float(resolution_expected[i][algNum]), resolution, 0.5*10**-SIZE_DECIMALS], 

446 

447 # sigma won't match exactly because 

448 # we're using skyvar=mean(var) instead of measured value ... expected a difference 

449 ["sigma", float(sigma_e_expected[i][algNum]), sigma, 0.07], 

450 ["shapeStatus", 0, flags, 0], 

451 ] 

452 

453 for test in tests: 

454 label, know, hsm, limit = test 

455 err = hsm - know 

456 msgTmp = "%-12s %s %5s: %6.6f %6.6f (val-known) = %.3g\n" % (algName, imageid, 

457 label, know, hsm, err) 

458 if not np.isfinite(err) or abs(err) > limit: 

459 msg += msgTmp 

460 nFail += 1 

461 

462 self.assertAlmostEqual(g1 if algName in ("KSB") else e1, galsim_e1[i][algNum], SHAPE_DECIMALS) 

463 self.assertAlmostEqual(g2 if algName in ("KSB") else e2, galsim_e2[i][algNum], SHAPE_DECIMALS) 

464 self.assertAlmostEqual(resolution, galsim_resolution[i][algNum], SIZE_DECIMALS) 

465 self.assertAlmostEqual(sigma, galsim_err[i][algNum], delta=0.07) 

466 

467 self.assertEqual(nFail, 0, "\n"+msg) 

468 

469 

470class PyGaussianPsf(afwDetection.Psf): 

471 # Like afwDetection.GaussianPsf, but handles computeImage exactly instead of 

472 # via interpolation. This is a subminimal implementation. It works for the 

473 # tests here but isn't fully functional as a Psf class. 

474 

475 def __init__(self, width, height, sigma, varyBBox=False, wrongBBox=False): 

476 afwDetection.Psf.__init__(self, isFixed=not varyBBox) 

477 self.dimensions = geom.Extent2I(width, height) 

478 self.sigma = sigma 

479 self.varyBBox = varyBBox # To address DM-29863 

480 self.wrongBBox = wrongBBox # To address DM-30426 

481 

482 def _doComputeKernelImage(self, position=None, color=None): 

483 bbox = self.computeBBox(position, color) 

484 img = afwImage.Image(bbox, dtype=np.float64) 

485 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1] 

486 rsqr = x**2 + y**2 

487 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2) 

488 img.array /= np.sum(img.array) 

489 return img 

490 

491 def _doComputeImage(self, position=None, color=None): 

492 bbox = self.computeBBox(position, color) 

493 if self.wrongBBox: 

494 # For DM-30426: 

495 # Purposely make computeImage.getBBox() and computeBBox() 

496 # inconsistent. Old shapeHSM code attempted to infer the former 

497 # from the latter, but was unreliable. New code infers the former 

498 # directly, so this inconsistency no longer breaks things. 

499 bbox.shift(geom.Extent2I(1, 1)) 

500 img = afwImage.Image(bbox, dtype=np.float64) 

501 y, x = np.ogrid[float(bbox.minY):bbox.maxY+1, bbox.minX:bbox.maxX+1] 

502 x -= (position.x - np.floor(position.x+0.5)) 

503 y -= (position.y - np.floor(position.y+0.5)) 

504 rsqr = x**2 + y**2 

505 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2) 

506 img.array /= np.sum(img.array) 

507 img.setXY0(geom.Point2I( 

508 img.getX0() + np.floor(position.x+0.5), 

509 img.getY0() + np.floor(position.y+0.5) 

510 )) 

511 return img 

512 

513 def _doComputeBBox(self, position=None, color=None): 

514 # Variable size bbox for addressing DM-29863 

515 dims = self.dimensions 

516 if self.varyBBox: 

517 if position.x > 20.0: 

518 dims = dims + geom.Extent2I(2, 2) 

519 return geom.Box2I(geom.Point2I(-dims/2), dims) 

520 

521 def _doComputeShape(self, position=None, color=None): 

522 return afwGeom.ellipses.Quadrupole(self.sigma**2, self.sigma**2, 0.0) 

523 

524 

525class PsfMomentsTestCase(unittest.TestCase): 

526 """A test case for PSF moments measurement""" 

527 

528 @staticmethod 

529 def computeDirectPsfMomentsFromGalSim( 

530 psf, 

531 center, 

532 useSourceCentroidOffset=False 

533 ): 

534 """Directly from GalSim.""" 

535 psfBBox = psf.computeImageBBox(center) 

536 psfSigma = psf.computeShape(center).getTraceRadius() 

537 if useSourceCentroidOffset: 

538 psfImage = psf.computeImage(center) 

539 centroid = center 

540 else: 

541 psfImage = psf.computeKernelImage(center) 

542 psfImage.setXY0(psfBBox.getMin()) 

543 centroid = geom.Point2D(psfBBox.getMin() + psfBBox.getDimensions() // 2) 

544 bbox = psfImage.getBBox(afwImage.PARENT) 

545 bounds = galsim.bounds.BoundsI(bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()) 

546 image = galsim.Image(psfImage.array, bounds=bounds, copy=False) 

547 guessCentroid = galsim.PositionD(centroid.x, centroid.y) 

548 shape = galsim.hsm.FindAdaptiveMom( 

549 image, 

550 weight=None, 

551 badpix=None, 

552 guess_sig=psfSigma, 

553 precision=1e-6, 

554 guess_centroid=guessCentroid, 

555 strict=True, 

556 round_moments=False, 

557 hsmparams=None, 

558 ) 

559 ellipse = lsst.afw.geom.ellipses.SeparableDistortionDeterminantRadius( 

560 e1=shape.observed_shape.e1, 

561 e2=shape.observed_shape.e2, 

562 radius=shape.moments_sigma, 

563 normalize=True, # Fail if |e|>1. 

564 ) 

565 quad = lsst.afw.geom.ellipses.Quadrupole(ellipse) 

566 ixx = quad.getIxx() 

567 iyy = quad.getIyy() 

568 ixy = quad.getIxy() 

569 return ixx, iyy, ixy 

570 

571 @lsst.utils.tests.methodParameters( 

572 # Make Cartesian product of settings to feed to methodParameters 

573 **dict(list(zip( 

574 (kwargs := dict( 

575 # Increasing the width beyond 4.5 leads to noticeable 

576 # truncation of the PSF, i.e. a PSF that is too large for the 

577 # box. While this truncated state leads to incorrect 

578 # measurements, it is necessary for testing purposes to 

579 # evaluate the behavior under these extreme conditions. 

580 width=(2.0, 3.0, 4.0, 10.0, 40.0, 100.0), 

581 useSourceCentroidOffset=(True, False), 

582 varyBBox=(True, False), 

583 wrongBBox=(True, False), 

584 center=( 

585 (23.0, 34.0), # various offsets that might cause trouble 

586 (23.5, 34.0), 

587 (23.5, 34.5), 

588 (23.15, 34.25), 

589 (22.81, 34.01), 

590 (22.81, 33.99), 

591 (1.2, 1.3), # psfImage extends outside exposure; that's okay 

592 (-100.0, -100.0), 

593 (-100.5, -100.0), 

594 (-100.5, -100.5), 

595 ) 

596 )).keys(), 

597 zip(*itertools.product(*kwargs.values())) 

598 ))) 

599 ) 

600 def testHsmPsfMoments( 

601 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

602 ): 

603 psf = PyGaussianPsf( 

604 35, 35, width, 

605 varyBBox=varyBBox, 

606 wrongBBox=wrongBBox 

607 ) 

608 exposure = afwImage.ExposureF(45, 56) 

609 exposure.getMaskedImage().set(1.0, 0, 1.0) 

610 exposure.setPsf(psf) 

611 

612 # perform the moment measurement 

613 algorithmName = "ext_shapeHSM_HsmPsfMoments" 

614 msConfig = base.SingleFrameMeasurementConfig() 

615 msConfig.algorithms.names = [algorithmName] 

616 control = msConfig.plugins[algorithmName] 

617 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass 

618 self.assertFalse(control.useSourceCentroidOffset) 

619 control.useSourceCentroidOffset = useSourceCentroidOffset 

620 plugin, cat = makePluginAndCat( 

621 alg, algorithmName, 

622 centroid="centroid", 

623 control=control, 

624 metadata=True, 

625 ) 

626 source = cat.addNew() 

627 source.set("centroid_x", center[0]) 

628 source.set("centroid_y", center[1]) 

629 offset = geom.Point2I(*center) 

630 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset) 

631 source.setFootprint(afwDetection.Footprint(tmpSpans)) 

632 plugin.measure(source, exposure) 

633 x = source.get("ext_shapeHSM_HsmPsfMoments_x") 

634 y = source.get("ext_shapeHSM_HsmPsfMoments_y") 

635 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx") 

636 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy") 

637 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy") 

638 

639 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag")) 

640 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_no_pixels")) 

641 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_not_contained")) 

642 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMoments_flag_parent_source")) 

643 

644 if width < 4.5: 

645 # i.e., as long as the PSF is not truncated for our 35x35 box. 

646 self.assertAlmostEqual(x, 0.0, 3) 

647 self.assertAlmostEqual(y, 0.0, 3) 

648 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0)) 

649 self.assertAlmostEqual(xx, expected.getIxx(), SHAPE_DECIMALS) 

650 self.assertAlmostEqual(xy, expected.getIxy(), SHAPE_DECIMALS) 

651 self.assertAlmostEqual(yy, expected.getIyy(), SHAPE_DECIMALS) 

652 

653 # Test schema documentation 

654 for fieldName in cat.schema.extract("*HsmPsfMoments_[xy]"): 

655 self.assertEqual(cat.schema[fieldName].asField().getDoc(), 

656 "Centroid of the PSF via the HSM shape algorithm") 

657 for fieldName in cat.schema.extract("*HsmPsfMoments_[xy][xy]*"): 

658 self.assertEqual(cat.schema[fieldName].asField().getDoc(), 

659 "Adaptive moments of the PSF via the HSM shape algorithm") 

660 

661 # Test that the moments are identical to those obtained directly by 

662 # GalSim. For `width` > 4.5 where the truncation becomes significant, 

663 # the answer might not be 'correct' but should remain 'consistent'. 

664 xxDirect, yyDirect, xyDirect = self.computeDirectPsfMomentsFromGalSim( 

665 psf, 

666 geom.Point2D(*center), 

667 useSourceCentroidOffset=useSourceCentroidOffset, 

668 ) 

669 self.assertEqual(xx, xxDirect) 

670 self.assertEqual(yy, yyDirect) 

671 self.assertEqual(xy, xyDirect) 

672 

673 @lsst.utils.tests.methodParameters( 

674 # Make Cartesian product of settings to feed to methodParameters 

675 **dict(list(zip( 

676 (kwargs := dict( 

677 width=(2.0, 3.0, 4.0), 

678 useSourceCentroidOffset=(True, False), 

679 varyBBox=(True, False), 

680 wrongBBox=(True, False), 

681 center=( 

682 (23.0, 34.0), # various offsets that might cause trouble 

683 (23.5, 34.0), 

684 (23.5, 34.5), 

685 (23.15, 34.25), 

686 (22.81, 34.01), 

687 (22.81, 33.99), 

688 ) 

689 )).keys(), 

690 zip(*itertools.product(*kwargs.values())) 

691 ))) 

692 ) 

693 def testHsmPsfMomentsDebiased( 

694 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

695 ): 

696 # As a note, it's really hard to actually unit test whether we've 

697 # succesfully "debiased" these measurements. That would require a 

698 # many-object comparison of moments with and without noise. So we just 

699 # test similar to the biased moments above. 

700 var = 1.2 

701 # As we reduce the flux, our deviation from the expected value 

702 # increases, so decrease tolerance. 

703 for flux, decimals in [ 

704 (1e6, 3), 

705 (1e4, 1), 

706 (1e3, 0), 

707 ]: 

708 psf = PyGaussianPsf( 

709 35, 35, width, 

710 varyBBox=varyBBox, 

711 wrongBBox=wrongBBox 

712 ) 

713 exposure = afwImage.ExposureF(45, 56) 

714 exposure.getMaskedImage().set(1.0, 0, var) 

715 exposure.setPsf(psf) 

716 

717 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

718 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass 

719 

720 # perform the shape measurement 

721 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedConfig() 

722 self.assertTrue(control.useSourceCentroidOffset) 

723 self.assertEqual(control.noiseSource, "variance") 

724 control.useSourceCentroidOffset = useSourceCentroidOffset 

725 plugin, cat = makePluginAndCat( 

726 alg, 

727 algorithmName, 

728 centroid="centroid", 

729 psfflux="base_PsfFlux", 

730 control=control, 

731 metadata=True, 

732 ) 

733 source = cat.addNew() 

734 source.set("centroid_x", center[0]) 

735 source.set("centroid_y", center[1]) 

736 offset = geom.Point2I(*center) 

737 source.set("base_PsfFlux_instFlux", flux) 

738 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset) 

739 source.setFootprint(afwDetection.Footprint(tmpSpans)) 

740 

741 plugin.measure(source, exposure) 

742 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

743 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

744 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

745 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

746 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

747 for flag in [ 

748 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

749 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

750 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

751 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

752 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

753 ]: 

754 self.assertFalse(source.get(flag)) 

755 

756 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0)) 

757 

758 self.assertAlmostEqual(x, 0.0, decimals) 

759 self.assertAlmostEqual(y, 0.0, decimals) 

760 

761 T = expected.getIxx() + expected.getIyy() 

762 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals) 

763 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals) 

764 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals) 

765 

766 # Repeat using noiseSource='meta'. Should get nearly the same 

767 # results if BGMEAN is set to `var` above. 

768 exposure2 = afwImage.ExposureF(45, 56) 

769 # set the variance plane to something else to ensure we're 

770 # ignoring it 

771 exposure2.getMaskedImage().set(1.0, 0, 2*var+1.1) 

772 exposure2.setPsf(psf) 

773 exposure2.getMetadata().set("BGMEAN", var) 

774 

775 control2 = shapeHSM.HsmPsfMomentsDebiasedConfig() 

776 control2.noiseSource = "meta" 

777 control2.useSourceCentroidOffset = useSourceCentroidOffset 

778 plugin2, cat2 = makePluginAndCat( 

779 alg, 

780 algorithmName, 

781 centroid="centroid", 

782 psfflux="base_PsfFlux", 

783 control=control2, 

784 metadata=True, 

785 ) 

786 source2 = cat2.addNew() 

787 source2.set("centroid_x", center[0]) 

788 source2.set("centroid_y", center[1]) 

789 offset2 = geom.Point2I(*center) 

790 source2.set("base_PsfFlux_instFlux", flux) 

791 tmpSpans2 = afwGeom.SpanSet.fromShape(int(width), offset=offset2) 

792 source2.setFootprint(afwDetection.Footprint(tmpSpans2)) 

793 

794 plugin2.measure(source2, exposure2) 

795 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

796 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

797 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

798 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

799 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

800 for flag in [ 

801 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

802 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

803 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

804 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

805 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

806 ]: 

807 self.assertFalse(source.get(flag)) 

808 

809 # Would be identically equal, but variance input via "BGMEAN" is 

810 # consumed in c++ as a double, where variance from the variance 

811 # plane is a c++ float. 

812 self.assertAlmostEqual(x, x2, 8) 

813 self.assertAlmostEqual(y, y2, 8) 

814 self.assertAlmostEqual(xx, xx2, 5) 

815 self.assertAlmostEqual(xy, xy2, 5) 

816 self.assertAlmostEqual(yy, yy2, 5) 

817 

818 # Test schema documentation 

819 for fieldName in cat.schema.extract("*HsmPsfMoments_[xy]"): 

820 self.assertEqual(cat.schema[fieldName].asField().getDoc(), 

821 "Debiased centroid of the PSF via the HSM shape algorithm") 

822 for fieldName in cat.schema.extract("*HsmPsfMoments_[xy][xy]*"): 

823 self.assertEqual(cat.schema[fieldName].asField().getDoc(), 

824 "Debiased adaptive moments of the PSF via the HSM shape algorithm") 

825 

826 testHsmPsfMomentsDebiasedEdgeArgs = dict( 

827 width=(2.0, 3.0, 4.0), 

828 useSourceCentroidOffset=(True, False), 

829 center=( 

830 (1.2, 1.3), 

831 (33.2, 50.1) 

832 ) 

833 ) 

834 

835 @lsst.utils.tests.methodParameters( 

836 # Make Cartesian product of settings to feed to methodParameters 

837 **dict(list(zip( 

838 (kwargs := dict( 

839 width=(2.0, 3.0, 4.0), 

840 useSourceCentroidOffset=(True, False), 

841 center=[ 

842 (1.2, 1.3), 

843 (33.2, 50.1) 

844 ] 

845 )).keys(), 

846 zip(*itertools.product(*kwargs.values())) 

847 ))) 

848 ) 

849 def testHsmPsfMomentsDebiasedEdge(self, width, useSourceCentroidOffset, center): 

850 # As we reduce the flux, our deviation from the expected value 

851 # increases, so decrease tolerance. 

852 var = 1.2 

853 for flux, decimals in [ 

854 (1e6, 3), 

855 (1e4, 2), 

856 (1e3, 1), 

857 ]: 

858 psf = PyGaussianPsf(35, 35, width) 

859 exposure = afwImage.ExposureF(45, 56) 

860 exposure.getMaskedImage().set(1.0, 0, 2*var+1.1) 

861 exposure.setPsf(psf) 

862 

863 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

864 alg = base.SingleFramePlugin.registry[algorithmName].PluginClass 

865 

866 # perform the shape measurement 

867 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

868 control.useSourceCentroidOffset = useSourceCentroidOffset 

869 self.assertEqual(control.noiseSource, "variance") 

870 plugin, cat = makePluginAndCat( 

871 alg, 

872 algorithmName, 

873 centroid="centroid", 

874 psfflux="base_PsfFlux", 

875 control=control, 

876 metadata=True, 

877 ) 

878 source = cat.addNew() 

879 source.set("centroid_x", center[0]) 

880 source.set("centroid_y", center[1]) 

881 offset = geom.Point2I(*center) 

882 source.set("base_PsfFlux_instFlux", flux) 

883 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset) 

884 source.setFootprint(afwDetection.Footprint(tmpSpans)) 

885 

886 # Edge fails when setting noise from var plane 

887 with self.assertRaises(base.MeasurementError): 

888 plugin.measure(source, exposure) 

889 

890 # Succeeds when noise is from meta 

891 exposure.getMetadata().set("BGMEAN", var) 

892 control.noiseSource = "meta" 

893 plugin, cat = makePluginAndCat( 

894 alg, 

895 algorithmName, 

896 centroid="centroid", 

897 psfflux="base_PsfFlux", 

898 control=control, 

899 metadata=True, 

900 ) 

901 source = cat.addNew() 

902 source.set("centroid_x", center[0]) 

903 source.set("centroid_y", center[1]) 

904 offset = geom.Point2I(*center) 

905 source.set("base_PsfFlux_instFlux", flux) 

906 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset) 

907 source.setFootprint(afwDetection.Footprint(tmpSpans)) 

908 plugin.measure(source, exposure) 

909 

910 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

911 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

912 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

913 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

914 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

915 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag")) 

916 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels")) 

917 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained")) 

918 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source")) 

919 # but _does_ set EDGE flag in this case 

920 self.assertTrue(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge")) 

921 

922 expected = afwEll.Quadrupole(afwEll.Axes(width, width, 0.0)) 

923 

924 self.assertAlmostEqual(x, 0.0, decimals) 

925 self.assertAlmostEqual(y, 0.0, decimals) 

926 

927 T = expected.getIxx() + expected.getIyy() 

928 self.assertAlmostEqual((xx-expected.getIxx())/T, 0.0, decimals) 

929 self.assertAlmostEqual((xy-expected.getIxy())/T, 0.0, decimals) 

930 self.assertAlmostEqual((yy-expected.getIyy())/T, 0.0, decimals) 

931 

932 # But fails hard if meta doesn't contain BGMEAN 

933 exposure.getMetadata().remove("BGMEAN") 

934 plugin, cat = makePluginAndCat( 

935 alg, 

936 algorithmName, 

937 centroid="centroid", 

938 psfflux="base_PsfFlux", 

939 control=control, 

940 metadata=True, 

941 ) 

942 source = cat.addNew() 

943 source.set("centroid_x", center[0]) 

944 source.set("centroid_y", center[1]) 

945 offset = geom.Point2I(*center) 

946 source.set("base_PsfFlux_instFlux", flux) 

947 tmpSpans = afwGeom.SpanSet.fromShape(int(width), offset=offset) 

948 source.setFootprint(afwDetection.Footprint(tmpSpans)) 

949 with self.assertRaises(base.FatalAlgorithmError): 

950 plugin.measure(source, exposure) 

951 

952 def testHsmPsfMomentsDebiasedBadNoiseSource(self): 

953 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

954 with self.assertRaises(pexConfig.FieldValidationError): 

955 control.noiseSource = "ACM" 

956 

957 

958class TestMemory(lsst.utils.tests.MemoryTestCase): 

959 pass 

960 

961 

962def setup_module(module): 

963 lsst.utils.tests.init() 

964 

965 

966if __name__ == "__main__": 966 ↛ 967line 966 didn't jump to line 967, because the condition on line 966 was never true

967 lsst.utils.tests.init() 

968 unittest.main()