Coverage for tests/test_hsm.py: 14%

506 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-17 08:48 +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, maskAll=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 if maskAll: 

182 msk.array[:] |= msk.getPlaneBitMask("BAD") 

183 

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

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

186 big.getImage().set(0) 

187 big.getMask().set(0) 

188 big.getVariance().set(v) 

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

190 subBig.assign(mimg) 

191 mimg = big 

192 mimg.setXY0(self.xy0) 

193 

194 exposure = afwImage.makeExposure(mimg) 

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

196 cdMatrix.shape = (2, 2) 

197 exposure.setWcs( 

198 afwGeom.makeSkyWcs( 

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

200 ) 

201 ) 

202 

203 # load the corresponding test psf 

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

205 psfImg = afwImage.ImageD(psfFile) 

206 psfImg -= self.bkgd 

207 

208 kernel = afwMath.FixedKernel(psfImg) 

209 kernelPsf = algorithms.KernelPsf(kernel) 

210 exposure.setPsf(kernelPsf) 

211 

212 # perform the shape measurement 

213 msConfig = base.SingleFrameMeasurementConfig() 

214 msConfig.plugins.names |= [algorithmName] 

215 control = msConfig.plugins[algorithmName] 

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

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

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

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

220 plugin, table = makePluginAndCat( 

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

222 ) 

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

224 source = table.makeRecord() 

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

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

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

228 plugin.measure(source, exposure) 

229 

230 return source 

231 

232 def testHsmSourceMoments(self): 

233 for i, imageid in enumerate(file_indices): 

234 source = self.runMeasurement( 

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

236 ) 

237 x = source.get("ext_shapeHSM_HsmSourceMoments_x") 

238 y = source.get("ext_shapeHSM_HsmSourceMoments_y") 

239 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx") 

240 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy") 

241 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy") 

242 

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

244 offset = self.xy0 + self.offset 

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

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

247 

248 expected = afwEll.Quadrupole( 

249 afwEll.SeparableDistortionDeterminantRadius( 

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

251 ) 

252 ) 

253 

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

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

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

257 

258 def testHsmSourceMomentsRound(self): 

259 for i, imageid in enumerate(file_indices): 

260 source = self.runMeasurement( 

261 "ext_shapeHSM_HsmSourceMomentsRound", 

262 imageid, 

263 x_centroid[i], 

264 y_centroid[i], 

265 sky_var[i], 

266 addFlux=True, 

267 ) 

268 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x") 

269 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y") 

270 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx") 

271 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy") 

272 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy") 

273 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux") 

274 

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

276 offset = self.xy0 + self.offset 

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

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

279 

280 expected = afwEll.Quadrupole( 

281 afwEll.SeparableDistortionDeterminantRadius( 

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

283 ) 

284 ) 

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

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

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

288 

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

290 

291 def testHsmSourceMomentsVsSdssShape(self): 

292 # Initialize a config and activate the plugins. 

293 sfmConfig = base.SingleFrameMeasurementConfig() 

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

295 

296 # Create a minimal schema (columns). 

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

298 

299 # Instantiate the task. 

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

301 

302 # Create a simple, test dataset. 

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

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

305 

306 # First source is a point. 

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

308 

309 # Second source is a galaxy. 

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

311 

312 # Third source is also a galaxy. 

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

314 

315 # Get the exposure and catalog. 

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

317 

318 # Run the measurement task. 

319 sfmTask.run(catalog, exposure) 

320 cat = catalog.asAstropy() 

321 

322 # Get the moments from the catalog. 

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

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

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

326 xxHsm, xyHsm, yyHsm = ( 

327 cat["ext_shapeHSM_HsmSourceMoments_xx"], 

328 cat["ext_shapeHSM_HsmSourceMoments_xy"], 

329 cat["ext_shapeHSM_HsmSourceMoments_yy"], 

330 ) 

331 

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

333 for i in range(3): 

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

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

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

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

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

339 

340 def testHsmSourceMomentsAllMasked(self): 

341 i = 0 

342 imageid = file_indices[0] 

343 with self.assertRaises(base.MeasurementError): 

344 _ = self.runMeasurement( 

345 "ext_shapeHSM_HsmSourceMoments", 

346 imageid, 

347 x_centroid[i], 

348 y_centroid[i], 

349 sky_var[i], 

350 maskAll=True, 

351 ) 

352 

353 

354class ShapeTestCase(unittest.TestCase): 

355 """A test case for shape measurement""" 

356 

357 def setUp(self): 

358 

359 # load the known values 

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

361 self.bkgd = 1000.0 # standard for atlas image 

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

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

364 

365 def tearDown(self): 

366 del self.offset 

367 del self.xy0 

368 

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

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

371 # load the test image 

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

373 img = afwImage.ImageF(imgFile) 

374 img -= self.bkgd 

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

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

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

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

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

380 

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

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

383 big.getImage().set(0) 

384 big.getMask().set(0) 

385 big.getVariance().set(v) 

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

387 subBig.assign(mimg) 

388 mimg = big 

389 mimg.setXY0(self.xy0) 

390 

391 exposure = afwImage.makeExposure(mimg) 

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

393 cdMatrix.shape = (2, 2) 

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

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

396 cdMatrix=cdMatrix)) 

397 

398 # load the corresponding test psf 

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

400 psfImg = afwImage.ImageD(psfFile) 

401 psfImg -= self.bkgd 

402 

403 kernel = afwMath.FixedKernel(psfImg) 

404 kernelPsf = algorithms.KernelPsf(kernel) 

405 exposure.setPsf(kernelPsf) 

406 

407 # perform the shape measurement 

408 msConfig = base.SingleFrameMeasurementConfig() 

409 msConfig.plugins.names |= [algorithmName] 

410 control = msConfig.plugins[algorithmName] 

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

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

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

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

415 plugin, table = makePluginAndCat(alg, algorithmName, control, centroid="centroid", metadata=True) 

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

417 source = table.makeRecord() 

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

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

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

421 plugin.measure(source, exposure) 

422 

423 # Get the trace radius of the PSF and GalSim images to use in the 

424 # EstimateShear call. 

425 bbox = source.getFootprint().getBBox() 

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

427 image = galsim.Image(exposure.image[bbox].array, bounds=bounds, copy=False) 

428 psf = galsim.Image(psfImg.array, copy=False) 

429 

430 # Retrieve the measurement "type" that Galsim outputs after estimation. 

431 # NOTE: not passing weight, badpix, sky_var, and some guess parameters 

432 # as the objective is solely to deduce the `meas_type` for this setup. 

433 postEstimationMeasType = galsim.hsm.EstimateShear( 

434 gal_image=image, 

435 PSF_image=psf, 

436 shear_est=control.shearType, 

437 guess_centroid=galsim.PositionD(center.getX(), center.getY()), 

438 strict=False, 

439 ).meas_type 

440 

441 return source, alg.measTypeSymbol, postEstimationMeasType 

442 

443 def testHsmShape(self): 

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

445 

446 nFail = 0 

447 msg = "" 

448 

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

450 enumerate(file_indices)): 

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

452 

453 source, preEstimationMeasType, postEstimationMeasType = self.runMeasurement( 

454 algorithmName, imageid, x_centroid[i], y_centroid[i], sky_var[i] 

455 ) 

456 

457 # Check consistency with GalSim output 

458 self.assertEqual( 

459 preEstimationMeasType, 

460 postEstimationMeasType, 

461 "The plugin setup is incompatible with GalSim output.", 

462 ) 

463 

464 ########################################## 

465 # see how we did 

466 if algName in ("KSB"): 

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

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

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

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

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

472 e1 = g1*scale 

473 e2 = g2*scale 

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

475 else: 

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

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

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

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

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

481 

482 tests = [ 

483 # label known-value measured tolerance 

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

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

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

487 

488 # sigma won't match exactly because 

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

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

491 ["shapeStatus", 0, flags, 0], 

492 ] 

493 

494 for test in tests: 

495 label, know, hsm, limit = test 

496 err = hsm - know 

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

498 label, know, hsm, err) 

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

500 msg += msgTmp 

501 nFail += 1 

502 

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

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

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

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

507 

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

509 

510 def testValidate(self): 

511 for algName in correction_methods: 

512 with self.assertRaises(pexConfig.FieldValidationError): 

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

514 msConfig = base.SingleFrameMeasurementConfig() 

515 msConfig.plugins.names |= [algorithmName] 

516 control = msConfig.plugins[algorithmName] 

517 control.shearType = "WRONG" 

518 control.validate() 

519 

520 

521class PyGaussianPsf(afwDetection.Psf): 

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

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

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

525 

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

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

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

529 self.sigma = sigma 

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

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

532 

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

534 bbox = self.computeBBox(position, color) 

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

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

537 rsqr = x**2 + y**2 

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

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

540 return img 

541 

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

543 bbox = self.computeBBox(position, color) 

544 if self.wrongBBox: 

545 # For DM-30426: 

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

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

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

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

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

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

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

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

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

555 rsqr = x**2 + y**2 

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

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

558 img.setXY0(geom.Point2I( 

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

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

561 )) 

562 return img 

563 

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

565 # Variable size bbox for addressing DM-29863 

566 dims = self.dimensions 

567 if self.varyBBox: 

568 if position.x > 20.0: 

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

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

571 

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

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

574 

575 

576class PsfMomentsTestCase(unittest.TestCase): 

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

578 

579 @staticmethod 

580 def computeDirectPsfMomentsFromGalSim( 

581 psf, 

582 center, 

583 useSourceCentroidOffset=False 

584 ): 

585 """Directly from GalSim.""" 

586 psfBBox = psf.computeImageBBox(center) 

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

588 if useSourceCentroidOffset: 

589 psfImage = psf.computeImage(center) 

590 centroid = center 

591 else: 

592 psfImage = psf.computeKernelImage(center) 

593 psfImage.setXY0(psfBBox.getMin()) 

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

595 bbox = psfImage.getBBox(afwImage.PARENT) 

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

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

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

599 shape = galsim.hsm.FindAdaptiveMom( 

600 image, 

601 weight=None, 

602 badpix=None, 

603 guess_sig=psfSigma, 

604 precision=1e-6, 

605 guess_centroid=guessCentroid, 

606 strict=True, 

607 round_moments=False, 

608 hsmparams=None, 

609 ) 

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

611 e1=shape.observed_shape.e1, 

612 e2=shape.observed_shape.e2, 

613 radius=shape.moments_sigma, 

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

615 ) 

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

617 ixx = quad.getIxx() 

618 iyy = quad.getIyy() 

619 ixy = quad.getIxy() 

620 return ixx, iyy, ixy 

621 

622 @lsst.utils.tests.methodParameters( 

623 # Make Cartesian product of settings to feed to methodParameters 

624 **dict(list(zip( 

625 (kwargs := dict( 

626 # Increasing the width beyond 4.5 leads to noticeable 

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

628 # box. While this truncated state leads to incorrect 

629 # measurements, it is necessary for testing purposes to 

630 # evaluate the behavior under these extreme conditions. 

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

632 useSourceCentroidOffset=(True, False), 

633 varyBBox=(True, False), 

634 wrongBBox=(True, False), 

635 center=( 

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

637 (23.5, 34.0), 

638 (23.5, 34.5), 

639 (23.15, 34.25), 

640 (22.81, 34.01), 

641 (22.81, 33.99), 

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

643 (-100.0, -100.0), 

644 (-100.5, -100.0), 

645 (-100.5, -100.5), 

646 ) 

647 )).keys(), 

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

649 ))) 

650 ) 

651 def testHsmPsfMoments( 

652 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

653 ): 

654 psf = PyGaussianPsf( 

655 35, 35, width, 

656 varyBBox=varyBBox, 

657 wrongBBox=wrongBBox 

658 ) 

659 exposure = afwImage.ExposureF(45, 56) 

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

661 exposure.setPsf(psf) 

662 

663 # perform the moment measurement 

664 algorithmName = "ext_shapeHSM_HsmPsfMoments" 

665 msConfig = base.SingleFrameMeasurementConfig() 

666 msConfig.algorithms.names = [algorithmName] 

667 control = msConfig.plugins[algorithmName] 

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

669 self.assertFalse(control.useSourceCentroidOffset) 

670 control.useSourceCentroidOffset = useSourceCentroidOffset 

671 plugin, cat = makePluginAndCat( 

672 alg, algorithmName, 

673 centroid="centroid", 

674 control=control, 

675 metadata=True, 

676 ) 

677 source = cat.addNew() 

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

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

680 offset = geom.Point2I(*center) 

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

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

683 plugin.measure(source, exposure) 

684 x = source.get("ext_shapeHSM_HsmPsfMoments_x") 

685 y = source.get("ext_shapeHSM_HsmPsfMoments_y") 

686 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx") 

687 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy") 

688 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy") 

689 

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

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

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

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

694 

695 if width < 4.5: 

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

697 self.assertAlmostEqual(x, 0.0, 3) 

698 self.assertAlmostEqual(y, 0.0, 3) 

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

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

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

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

703 

704 # Test schema documentation 

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

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

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

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

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

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

711 

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

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

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

715 xxDirect, yyDirect, xyDirect = self.computeDirectPsfMomentsFromGalSim( 

716 psf, 

717 geom.Point2D(*center), 

718 useSourceCentroidOffset=useSourceCentroidOffset, 

719 ) 

720 self.assertEqual(xx, xxDirect) 

721 self.assertEqual(yy, yyDirect) 

722 self.assertEqual(xy, xyDirect) 

723 

724 @lsst.utils.tests.methodParameters( 

725 # Make Cartesian product of settings to feed to methodParameters 

726 **dict(list(zip( 

727 (kwargs := dict( 

728 width=(2.0, 3.0, 4.0), 

729 useSourceCentroidOffset=(True, False), 

730 varyBBox=(True, False), 

731 wrongBBox=(True, False), 

732 center=( 

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

734 (23.5, 34.0), 

735 (23.5, 34.5), 

736 (23.15, 34.25), 

737 (22.81, 34.01), 

738 (22.81, 33.99), 

739 ) 

740 )).keys(), 

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

742 ))) 

743 ) 

744 def testHsmPsfMomentsDebiased( 

745 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

746 ): 

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

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

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

750 # test similar to the biased moments above. 

751 var = 1.2 

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

753 # increases, so decrease tolerance. 

754 for flux, decimals in [ 

755 (1e6, 3), 

756 (1e4, 1), 

757 (1e3, 0), 

758 ]: 

759 psf = PyGaussianPsf( 

760 35, 35, width, 

761 varyBBox=varyBBox, 

762 wrongBBox=wrongBBox 

763 ) 

764 exposure = afwImage.ExposureF(45, 56) 

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

766 exposure.setPsf(psf) 

767 

768 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

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

770 

771 # perform the shape measurement 

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

773 self.assertTrue(control.useSourceCentroidOffset) 

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

775 control.useSourceCentroidOffset = useSourceCentroidOffset 

776 plugin, cat = makePluginAndCat( 

777 alg, 

778 algorithmName, 

779 centroid="centroid", 

780 psfflux="base_PsfFlux", 

781 control=control, 

782 metadata=True, 

783 ) 

784 source = cat.addNew() 

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

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

787 offset = geom.Point2I(*center) 

788 source.set("base_PsfFlux_instFlux", flux) 

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

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

791 

792 plugin.measure(source, exposure) 

793 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

794 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

795 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

796 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

797 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

798 for flag in [ 

799 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

800 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

801 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

802 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

803 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

804 ]: 

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

806 

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

808 

809 self.assertAlmostEqual(x, 0.0, decimals) 

810 self.assertAlmostEqual(y, 0.0, decimals) 

811 

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

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

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

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

816 

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

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

819 exposure2 = afwImage.ExposureF(45, 56) 

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

821 # ignoring it 

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

823 exposure2.setPsf(psf) 

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

825 

826 control2 = shapeHSM.HsmPsfMomentsDebiasedConfig() 

827 control2.noiseSource = "meta" 

828 control2.useSourceCentroidOffset = useSourceCentroidOffset 

829 plugin2, cat2 = makePluginAndCat( 

830 alg, 

831 algorithmName, 

832 centroid="centroid", 

833 psfflux="base_PsfFlux", 

834 control=control2, 

835 metadata=True, 

836 ) 

837 source2 = cat2.addNew() 

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

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

840 offset2 = geom.Point2I(*center) 

841 source2.set("base_PsfFlux_instFlux", flux) 

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

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

844 

845 plugin2.measure(source2, exposure2) 

846 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

847 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

848 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

849 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

850 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

851 for flag in [ 

852 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

853 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

854 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

855 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

856 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

857 ]: 

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

859 

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

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

862 # plane is a c++ float. 

863 self.assertAlmostEqual(x, x2, 8) 

864 self.assertAlmostEqual(y, y2, 8) 

865 self.assertAlmostEqual(xx, xx2, 5) 

866 self.assertAlmostEqual(xy, xy2, 5) 

867 self.assertAlmostEqual(yy, yy2, 5) 

868 

869 # Test schema documentation 

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

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

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

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

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

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

876 

877 testHsmPsfMomentsDebiasedEdgeArgs = dict( 

878 width=(2.0, 3.0, 4.0), 

879 useSourceCentroidOffset=(True, False), 

880 center=( 

881 (1.2, 1.3), 

882 (33.2, 50.1) 

883 ) 

884 ) 

885 

886 @lsst.utils.tests.methodParameters( 

887 # Make Cartesian product of settings to feed to methodParameters 

888 **dict(list(zip( 

889 (kwargs := dict( 

890 width=(2.0, 3.0, 4.0), 

891 useSourceCentroidOffset=(True, False), 

892 center=[ 

893 (1.2, 1.3), 

894 (33.2, 50.1) 

895 ] 

896 )).keys(), 

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

898 ))) 

899 ) 

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

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

902 # increases, so decrease tolerance. 

903 var = 1.2 

904 for flux, decimals in [ 

905 (1e6, 3), 

906 (1e4, 2), 

907 (1e3, 1), 

908 ]: 

909 psf = PyGaussianPsf(35, 35, width) 

910 exposure = afwImage.ExposureF(45, 56) 

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

912 exposure.setPsf(psf) 

913 

914 algorithmName = "ext_shapeHSM_HsmPsfMomentsDebiased" 

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

916 

917 # perform the shape measurement 

918 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

919 control.useSourceCentroidOffset = useSourceCentroidOffset 

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

921 plugin, cat = makePluginAndCat( 

922 alg, 

923 algorithmName, 

924 centroid="centroid", 

925 psfflux="base_PsfFlux", 

926 control=control, 

927 metadata=True, 

928 ) 

929 source = cat.addNew() 

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

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

932 offset = geom.Point2I(*center) 

933 source.set("base_PsfFlux_instFlux", flux) 

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

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

936 

937 # Edge fails when setting noise from var plane 

938 with self.assertRaises(base.MeasurementError): 

939 plugin.measure(source, exposure) 

940 

941 # Succeeds when noise is from meta 

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

943 control.noiseSource = "meta" 

944 plugin, cat = makePluginAndCat( 

945 alg, 

946 algorithmName, 

947 centroid="centroid", 

948 psfflux="base_PsfFlux", 

949 control=control, 

950 metadata=True, 

951 ) 

952 source = cat.addNew() 

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

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

955 offset = geom.Point2I(*center) 

956 source.set("base_PsfFlux_instFlux", flux) 

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

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

959 plugin.measure(source, exposure) 

960 

961 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

962 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

963 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

964 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

965 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

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

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

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

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

970 # but _does_ set EDGE flag in this case 

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

972 

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

974 

975 self.assertAlmostEqual(x, 0.0, decimals) 

976 self.assertAlmostEqual(y, 0.0, decimals) 

977 

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

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

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

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

982 

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

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

985 plugin, cat = makePluginAndCat( 

986 alg, 

987 algorithmName, 

988 centroid="centroid", 

989 psfflux="base_PsfFlux", 

990 control=control, 

991 metadata=True, 

992 ) 

993 source = cat.addNew() 

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

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

996 offset = geom.Point2I(*center) 

997 source.set("base_PsfFlux_instFlux", flux) 

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

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

1000 with self.assertRaises(base.FatalAlgorithmError): 

1001 plugin.measure(source, exposure) 

1002 

1003 def testHsmPsfMomentsDebiasedBadNoiseSource(self): 

1004 control = shapeHSM.HsmPsfMomentsDebiasedConfig() 

1005 with self.assertRaises(pexConfig.FieldValidationError): 

1006 control.noiseSource = "ACM" 

1007 

1008 

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

1010 pass 

1011 

1012 

1013def setup_module(module): 

1014 lsst.utils.tests.init() 

1015 

1016 

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

1018 lsst.utils.tests.init() 

1019 unittest.main()