Coverage for tests/test_hsm.py: 14%

484 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-05 12:20 +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 if control is None: 

136 control = alg.ConfigClass() 

137 if addFlux: 

138 control.addFlux = True 

139 schema = afwTable.SourceTable.makeMinimalSchema() 

140 if centroid: 

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

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 

154 

155 

156class MomentsTestCase(unittest.TestCase): 

157 """A test case for shape measurement""" 

158 

159 def setUp(self): 

160 # load the known values 

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

162 self.bkgd = 1000.0 # standard for atlas image 

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

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

165 

166 def tearDown(self): 

167 del self.offset 

168 del self.xy0 

169 

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

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

172 # load the test image 

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

174 img = afwImage.ImageF(imgFile) 

175 img -= self.bkgd 

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

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

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

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

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

181 

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

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

184 big.getImage().set(0) 

185 big.getMask().set(0) 

186 big.getVariance().set(v) 

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

188 subBig.assign(mimg) 

189 mimg = big 

190 mimg.setXY0(self.xy0) 

191 

192 exposure = afwImage.makeExposure(mimg) 

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

194 cdMatrix.shape = (2, 2) 

195 exposure.setWcs( 

196 afwGeom.makeSkyWcs( 

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

198 ) 

199 ) 

200 

201 # load the corresponding test psf 

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

203 psfImg = afwImage.ImageD(psfFile) 

204 psfImg -= self.bkgd 

205 

206 kernel = afwMath.FixedKernel(psfImg) 

207 kernelPsf = algorithms.KernelPsf(kernel) 

208 exposure.setPsf(kernelPsf) 

209 

210 # perform the shape measurement 

211 msConfig = base.SingleFrameMeasurementConfig() 

212 msConfig.plugins.names |= [algorithmName] 

213 control = msConfig.plugins[algorithmName] 

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

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

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

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

218 plugin, table = makePluginAndCat( 

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

220 ) 

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

222 source = table.makeRecord() 

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

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

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

226 plugin.measure(source, exposure) 

227 

228 return source 

229 

230 def testHsmSourceMoments(self): 

231 for i, imageid in enumerate(file_indices): 

232 source = self.runMeasurement( 

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

234 ) 

235 x = source.get("ext_shapeHSM_HsmSourceMoments_x") 

236 y = source.get("ext_shapeHSM_HsmSourceMoments_y") 

237 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx") 

238 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy") 

239 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy") 

240 

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

242 offset = self.xy0 + self.offset 

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

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

245 

246 expected = afwEll.Quadrupole( 

247 afwEll.SeparableDistortionDeterminantRadius( 

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

249 ) 

250 ) 

251 

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

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

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

255 

256 def testHsmSourceMomentsRound(self): 

257 for i, imageid in enumerate(file_indices): 

258 source = self.runMeasurement( 

259 "ext_shapeHSM_HsmSourceMomentsRound", 

260 imageid, 

261 x_centroid[i], 

262 y_centroid[i], 

263 sky_var[i], 

264 addFlux=True, 

265 ) 

266 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x") 

267 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y") 

268 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx") 

269 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy") 

270 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy") 

271 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux") 

272 

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

274 offset = self.xy0 + self.offset 

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

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

277 

278 expected = afwEll.Quadrupole( 

279 afwEll.SeparableDistortionDeterminantRadius( 

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

281 ) 

282 ) 

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

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

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

286 

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

288 

289 def testHsmSourceMomentsVsSdssShape(self): 

290 # Initialize a config and activate the plugins. 

291 sfmConfig = base.SingleFrameMeasurementConfig() 

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

293 

294 # Create a minimal schema (columns). 

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

296 

297 # Instantiate the task. 

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

299 

300 # Create a simple, test dataset. 

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

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

303 

304 # First source is a point. 

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

306 

307 # Second source is a galaxy. 

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

309 

310 # Third source is also a galaxy. 

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

312 

313 # Get the exposure and catalog. 

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

315 

316 # Run the measurement task. 

317 sfmTask.run(catalog, exposure) 

318 cat = catalog.asAstropy() 

319 

320 # Get the moments from the catalog. 

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

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

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

324 xxHsm, xyHsm, yyHsm = ( 

325 cat["ext_shapeHSM_HsmSourceMoments_xx"], 

326 cat["ext_shapeHSM_HsmSourceMoments_xy"], 

327 cat["ext_shapeHSM_HsmSourceMoments_yy"], 

328 ) 

329 

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

331 for i in range(3): 

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

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

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

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

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

337 

338 

339class ShapeTestCase(unittest.TestCase): 

340 """A test case for shape measurement""" 

341 

342 def setUp(self): 

343 

344 # load the known values 

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

346 self.bkgd = 1000.0 # standard for atlas image 

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

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

349 

350 def tearDown(self): 

351 del self.offset 

352 del self.xy0 

353 

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

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

356 # load the test image 

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

358 img = afwImage.ImageF(imgFile) 

359 img -= self.bkgd 

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

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

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

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

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

365 

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

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

368 big.getImage().set(0) 

369 big.getMask().set(0) 

370 big.getVariance().set(v) 

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

372 subBig.assign(mimg) 

373 mimg = big 

374 mimg.setXY0(self.xy0) 

375 

376 exposure = afwImage.makeExposure(mimg) 

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

378 cdMatrix.shape = (2, 2) 

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

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

381 cdMatrix=cdMatrix)) 

382 

383 # load the corresponding test psf 

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

385 psfImg = afwImage.ImageD(psfFile) 

386 psfImg -= self.bkgd 

387 

388 kernel = afwMath.FixedKernel(psfImg) 

389 kernelPsf = algorithms.KernelPsf(kernel) 

390 exposure.setPsf(kernelPsf) 

391 

392 # perform the shape measurement 

393 msConfig = base.SingleFrameMeasurementConfig() 

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

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

396 msConfig.algorithms.names = [algorithmName] 

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

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

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

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

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

402 source = table.makeRecord() 

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

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

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

406 plugin.measure(source, exposure) 

407 

408 return source 

409 

410 def testHsmShape(self): 

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

412 

413 nFail = 0 

414 msg = "" 

415 

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

417 enumerate(file_indices)): 

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

419 

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

421 

422 ########################################## 

423 # see how we did 

424 if algName in ("KSB"): 

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

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

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

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

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

430 e1 = g1*scale 

431 e2 = g2*scale 

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

433 else: 

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

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

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

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

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

439 

440 tests = [ 

441 # label known-value measured tolerance 

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

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

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

445 

446 # sigma won't match exactly because 

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

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

449 ["shapeStatus", 0, flags, 0], 

450 ] 

451 

452 for test in tests: 

453 label, know, hsm, limit = test 

454 err = hsm - know 

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

456 label, know, hsm, err) 

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

458 msg += msgTmp 

459 nFail += 1 

460 

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

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

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

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

465 

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

467 

468 

469class PyGaussianPsf(afwDetection.Psf): 

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

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

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

473 

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

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

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

477 self.sigma = sigma 

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

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

480 

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

482 bbox = self.computeBBox(position, color) 

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

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

485 rsqr = x**2 + y**2 

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

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

488 return img 

489 

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

491 bbox = self.computeBBox(position, color) 

492 if self.wrongBBox: 

493 # For DM-30426: 

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

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

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

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

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

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

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

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

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

503 rsqr = x**2 + y**2 

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

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

506 img.setXY0(geom.Point2I( 

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

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

509 )) 

510 return img 

511 

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

513 # Variable size bbox for addressing DM-29863 

514 dims = self.dimensions 

515 if self.varyBBox: 

516 if position.x > 20.0: 

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

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

519 

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

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

522 

523 

524class PsfMomentsTestCase(unittest.TestCase): 

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

526 

527 @staticmethod 

528 def computeDirectPsfMomentsFromGalSim( 

529 psf, 

530 center, 

531 useSourceCentroidOffset=False 

532 ): 

533 """Directly from GalSim.""" 

534 psfBBox = psf.computeImageBBox(center) 

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

536 if useSourceCentroidOffset: 

537 psfImage = psf.computeImage(center) 

538 centroid = center 

539 else: 

540 psfImage = psf.computeKernelImage(center) 

541 psfImage.setXY0(psfBBox.getMin()) 

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

543 bbox = psfImage.getBBox(afwImage.PARENT) 

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

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

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

547 shape = galsim.hsm.FindAdaptiveMom( 

548 image, 

549 weight=None, 

550 badpix=None, 

551 guess_sig=psfSigma, 

552 precision=1e-6, 

553 guess_centroid=guessCentroid, 

554 strict=True, 

555 round_moments=False, 

556 hsmparams=None, 

557 ) 

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

559 e1=shape.observed_shape.e1, 

560 e2=shape.observed_shape.e2, 

561 radius=shape.moments_sigma, 

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

563 ) 

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

565 ixx = quad.getIxx() 

566 iyy = quad.getIyy() 

567 ixy = quad.getIxy() 

568 return ixx, iyy, ixy 

569 

570 @lsst.utils.tests.methodParameters( 

571 # Make Cartesian product of settings to feed to methodParameters 

572 **dict(list(zip( 

573 (kwargs := dict( 

574 # Increasing the width beyond 4.5 leads to noticeable 

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

576 # box. While this truncated state leads to incorrect 

577 # measurements, it is necessary for testing purposes to 

578 # evaluate the behavior under these extreme conditions. 

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

580 useSourceCentroidOffset=(True, False), 

581 varyBBox=(True, False), 

582 wrongBBox=(True, False), 

583 center=( 

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

585 (23.5, 34.0), 

586 (23.5, 34.5), 

587 (23.15, 34.25), 

588 (22.81, 34.01), 

589 (22.81, 33.99), 

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

591 (-100.0, -100.0), 

592 (-100.5, -100.0), 

593 (-100.5, -100.5), 

594 ) 

595 )).keys(), 

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

597 ))) 

598 ) 

599 def testHsmPsfMoments( 

600 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

601 ): 

602 psf = PyGaussianPsf( 

603 35, 35, width, 

604 varyBBox=varyBBox, 

605 wrongBBox=wrongBBox 

606 ) 

607 exposure = afwImage.ExposureF(45, 56) 

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

609 exposure.setPsf(psf) 

610 

611 # perform the moment measurement 

612 algorithmName = "ext_shapeHSM_HsmPsfMoments" 

613 msConfig = base.SingleFrameMeasurementConfig() 

614 msConfig.algorithms.names = [algorithmName] 

615 control = msConfig.plugins[algorithmName] 

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

617 self.assertFalse(control.useSourceCentroidOffset) 

618 control.useSourceCentroidOffset = useSourceCentroidOffset 

619 plugin, cat = makePluginAndCat( 

620 alg, algorithmName, 

621 centroid="centroid", 

622 control=control, 

623 metadata=True, 

624 ) 

625 source = cat.addNew() 

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

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

628 offset = geom.Point2I(*center) 

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

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

631 plugin.measure(source, exposure) 

632 x = source.get("ext_shapeHSM_HsmPsfMoments_x") 

633 y = source.get("ext_shapeHSM_HsmPsfMoments_y") 

634 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx") 

635 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy") 

636 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy") 

637 

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

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

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

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

642 

643 if width < 4.5: 

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

645 self.assertAlmostEqual(x, 0.0, 3) 

646 self.assertAlmostEqual(y, 0.0, 3) 

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

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

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

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

651 

652 # Test schema documentation 

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

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

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

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

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

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

659 

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

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

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

663 xxDirect, yyDirect, xyDirect = self.computeDirectPsfMomentsFromGalSim( 

664 psf, 

665 geom.Point2D(*center), 

666 useSourceCentroidOffset=useSourceCentroidOffset, 

667 ) 

668 self.assertEqual(xx, xxDirect) 

669 self.assertEqual(yy, yyDirect) 

670 self.assertEqual(xy, xyDirect) 

671 

672 @lsst.utils.tests.methodParameters( 

673 # Make Cartesian product of settings to feed to methodParameters 

674 **dict(list(zip( 

675 (kwargs := dict( 

676 width=(2.0, 3.0, 4.0), 

677 useSourceCentroidOffset=(True, False), 

678 varyBBox=(True, False), 

679 wrongBBox=(True, False), 

680 center=( 

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

682 (23.5, 34.0), 

683 (23.5, 34.5), 

684 (23.15, 34.25), 

685 (22.81, 34.01), 

686 (22.81, 33.99), 

687 ) 

688 )).keys(), 

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

690 ))) 

691 ) 

692 def testHsmPsfMomentsDebiased( 

693 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

694 ): 

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

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

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

698 # test similar to the biased moments above. 

699 var = 1.2 

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

701 # increases, so decrease tolerance. 

702 for flux, decimals in [ 

703 (1e6, 3), 

704 (1e4, 1), 

705 (1e3, 0), 

706 ]: 

707 psf = PyGaussianPsf( 

708 35, 35, width, 

709 varyBBox=varyBBox, 

710 wrongBBox=wrongBBox 

711 ) 

712 exposure = afwImage.ExposureF(45, 56) 

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

714 exposure.setPsf(psf) 

715 

716 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

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

718 

719 # perform the shape measurement 

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

721 self.assertTrue(control.useSourceCentroidOffset) 

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

723 control.useSourceCentroidOffset = useSourceCentroidOffset 

724 plugin, cat = makePluginAndCat( 

725 alg, 

726 algorithmName, 

727 centroid="centroid", 

728 psfflux="base_PsfFlux", 

729 control=control, 

730 metadata=True, 

731 ) 

732 source = cat.addNew() 

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

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

735 offset = geom.Point2I(*center) 

736 source.set("base_PsfFlux_instFlux", flux) 

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

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

739 

740 plugin.measure(source, exposure) 

741 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

742 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

743 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

744 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

745 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

746 for flag in [ 

747 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

748 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

749 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

750 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

751 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

752 ]: 

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

754 

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

756 

757 self.assertAlmostEqual(x, 0.0, decimals) 

758 self.assertAlmostEqual(y, 0.0, decimals) 

759 

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

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

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

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

764 

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

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

767 exposure2 = afwImage.ExposureF(45, 56) 

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

769 # ignoring it 

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

771 exposure2.setPsf(psf) 

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

773 

774 control2 = shapeHSM.HsmPsfMomentsDebiasedConfig() 

775 control2.noiseSource = "meta" 

776 control2.useSourceCentroidOffset = useSourceCentroidOffset 

777 plugin2, cat2 = makePluginAndCat( 

778 alg, 

779 algorithmName, 

780 centroid="centroid", 

781 psfflux="base_PsfFlux", 

782 control=control2, 

783 metadata=True, 

784 ) 

785 source2 = cat2.addNew() 

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

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

788 offset2 = geom.Point2I(*center) 

789 source2.set("base_PsfFlux_instFlux", flux) 

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

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

792 

793 plugin2.measure(source2, exposure2) 

794 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

795 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

796 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

797 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

798 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

799 for flag in [ 

800 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

801 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

802 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

803 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

804 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

805 ]: 

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

807 

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

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

810 # plane is a c++ float. 

811 self.assertAlmostEqual(x, x2, 8) 

812 self.assertAlmostEqual(y, y2, 8) 

813 self.assertAlmostEqual(xx, xx2, 5) 

814 self.assertAlmostEqual(xy, xy2, 5) 

815 self.assertAlmostEqual(yy, yy2, 5) 

816 

817 # Test schema documentation 

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

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

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

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

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

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

824 

825 testHsmPsfMomentsDebiasedEdgeArgs = dict( 

826 width=(2.0, 3.0, 4.0), 

827 useSourceCentroidOffset=(True, False), 

828 center=( 

829 (1.2, 1.3), 

830 (33.2, 50.1) 

831 ) 

832 ) 

833 

834 @lsst.utils.tests.methodParameters( 

835 # Make Cartesian product of settings to feed to methodParameters 

836 **dict(list(zip( 

837 (kwargs := dict( 

838 width=(2.0, 3.0, 4.0), 

839 useSourceCentroidOffset=(True, False), 

840 center=[ 

841 (1.2, 1.3), 

842 (33.2, 50.1) 

843 ] 

844 )).keys(), 

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

846 ))) 

847 ) 

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

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

850 # increases, so decrease tolerance. 

851 var = 1.2 

852 for flux, decimals in [ 

853 (1e6, 3), 

854 (1e4, 2), 

855 (1e3, 1), 

856 ]: 

857 psf = PyGaussianPsf(35, 35, width) 

858 exposure = afwImage.ExposureF(45, 56) 

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

860 exposure.setPsf(psf) 

861 

862 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

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

864 

865 # perform the shape measurement 

866 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

867 control.useSourceCentroidOffset = useSourceCentroidOffset 

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

869 plugin, cat = makePluginAndCat( 

870 alg, 

871 algorithmName, 

872 centroid="centroid", 

873 psfflux="base_PsfFlux", 

874 control=control, 

875 metadata=True, 

876 ) 

877 source = cat.addNew() 

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

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

880 offset = geom.Point2I(*center) 

881 source.set("base_PsfFlux_instFlux", flux) 

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

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

884 

885 # Edge fails when setting noise from var plane 

886 with self.assertRaises(base.MeasurementError): 

887 plugin.measure(source, exposure) 

888 

889 # Succeeds when noise is from meta 

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

891 control.noiseSource = "meta" 

892 plugin, cat = makePluginAndCat( 

893 alg, 

894 algorithmName, 

895 centroid="centroid", 

896 psfflux="base_PsfFlux", 

897 control=control, 

898 metadata=True, 

899 ) 

900 source = cat.addNew() 

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

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

903 offset = geom.Point2I(*center) 

904 source.set("base_PsfFlux_instFlux", flux) 

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

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

907 plugin.measure(source, exposure) 

908 

909 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

910 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

911 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

912 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

913 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

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

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

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

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

918 # but _does_ set EDGE flag in this case 

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

920 

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

922 

923 self.assertAlmostEqual(x, 0.0, decimals) 

924 self.assertAlmostEqual(y, 0.0, decimals) 

925 

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

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

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

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

930 

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

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

933 plugin, cat = makePluginAndCat( 

934 alg, 

935 algorithmName, 

936 centroid="centroid", 

937 psfflux="base_PsfFlux", 

938 control=control, 

939 metadata=True, 

940 ) 

941 source = cat.addNew() 

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

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

944 offset = geom.Point2I(*center) 

945 source.set("base_PsfFlux_instFlux", flux) 

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

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

948 with self.assertRaises(base.FatalAlgorithmError): 

949 plugin.measure(source, exposure) 

950 

951 def testHsmPsfMomentsDebiasedBadNoiseSource(self): 

952 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

953 with self.assertRaises(pexConfig.FieldValidationError): 

954 control.noiseSource = "ACM" 

955 

956 

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

958 pass 

959 

960 

961def setup_module(module): 

962 lsst.utils.tests.init() 

963 

964 

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

966 lsst.utils.tests.init() 

967 unittest.main()