Coverage for tests/test_hsm.py: 15%

374 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-30 02:30 -0700

1#!/usr/bin/env python 

2# 

3# LSST Data Management System 

4# 

5# Copyright 2008-2016 AURA/LSST. 

6# 

7# This product includes software developed by the 

8# LSST Project (http://www.lsst.org/). 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the LSST License Statement and 

21# the GNU General Public License along with this program. If not, 

22# see <https://www.lsstcorp.org/LegalNotices/>. 

23# 

24import os 

25import numpy as np 

26import unittest 

27import itertools 

28 

29import lsst.afw.image as afwImage 

30import lsst.afw.math as afwMath 

31from lsst.daf.base import PropertySet 

32import lsst.meas.base as base 

33import lsst.meas.algorithms as algorithms 

34import lsst.afw.detection as afwDetection 

35import lsst.afw.table as afwTable 

36import lsst.afw.geom as afwGeom 

37import lsst.geom as geom 

38import lsst.afw.geom.ellipses as afwEll 

39import lsst.utils.tests 

40import lsst.meas.extensions.shapeHSM 

41 

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

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

44 

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

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

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

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

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

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

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

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

53e1_expected = np.array([ 

54 [0.467603106752, 0.381211727, 0.398856937, 0.401755571], 

55 [0.28618443944, 0.199222784, 0.233883543, 0.234257525], 

56 [0.271533794146, 0.158049396, 0.183517068, 0.184893412], 

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

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

59e2_expected = np.array([ 

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

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

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

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

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

65resolution_expected = np.array([ 

66 [0.796144249, 0.835624917, 0.835624917, 0.827796187], 

67 [0.685023735, 0.699602704, 0.699602704, 0.659457638], 

68 [0.634736458, 0.651040481, 0.651040481, 0.614663396], 

69 [0.477027015, 0.477210752, 0.477210752, 0.423157447], 

70 [0.595205998, 0.611824797, 0.611824797, 0.563582092]]) 

71sigma_e_expected = np.array([ 

72 [0.016924826, 0.014637648, 0.014637648, 0.014465546], 

73 [0.075769504, 0.073602324, 0.073602324, 0.064414520], 

74 [0.110253112, 0.106222900, 0.106222900, 0.099357106], 

75 [0.185276702, 0.184300955, 0.184300955, 0.173478300], 

76 [0.073020065, 0.070270966, 0.070270966, 0.061856263]]) 

77# End of GalSim's values 

78 

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

80galsim_e1 = np.array([ 

81 [0.399292618036, 0.381213068962, 0.398856908083, 0.401749581099], 

82 [0.155929282308, 0.199228107929, 0.233882278204, 0.234371587634], 

83 [0.150018423796, 0.158052951097, 0.183515056968, 0.184561833739], 

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

85 [0.33959621191, 0.374140143394, 0.713756918907, 0.43560180068], 

86]) 

87galsim_e2 = np.array([ 

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

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

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

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

92 [0.320115834475, 0.370669454336, 0.702303349972, 0.433968126774], 

93]) 

94galsim_resolution = np.array([ 

95 [0.79614430666, 0.835625052452, 0.835625052452, 0.827822327614], 

96 [0.685023903847, 0.699601829052, 0.699601829052, 0.659438848495], 

97 [0.634736537933, 0.651039719582, 0.651039719582, 0.614759743214], 

98 [0.477026551962, 0.47721144557, 0.47721144557, 0.423227936029], 

99 [0.595205545425, 0.611821532249, 0.611821532249, 0.563564240932], 

100]) 

101galsim_err = np.array([ 

102 [0.0169247947633, 0.0146376201883, 0.0146376201883, 0.0144661813974], 

103 [0.0757696777582, 0.0736026018858, 0.0736026018858, 0.0644160583615], 

104 [0.110252402723, 0.106222368777, 0.106222368777, 0.0993555411696], 

105 [0.185278102756, 0.184301897883, 0.184301897883, 0.17346136272], 

106 [0.0730196461082, 0.0702708885074, 0.0702708885074, 0.0618583671749], 

107]) 

108 

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

110 [2.24490427971, 0.336240686301, -0.627372910656], 

111 [1.9031778574, 0.150566105384, -0.245272792302], 

112 [1.77790760994, 0.112286123389, -0.286203939641], 

113 [1.45464873314, -0.155597168978, -0.102008266223], 

114 [1.63144648075, 0.22886961923, 0.228813588897], 

115]) 

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

117 [36.218247328, 20.5678722157], 

118 [20.325744838, 25.4176650386], 

119 [9.54257706283, 12.6134786199], 

120 [20.6407850048, 39.5864802706], 

121 [58.5008586442, 28.2850942049], 

122]) 

123 

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

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

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

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

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

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

130]) 

131 

132 

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

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

135 if control is None: 

136 control = alg.ConfigClass() 

137 schema = afwTable.SourceTable.makeMinimalSchema() 

138 if centroid: 

139 lsst.afw.table.Point2DKey.addFields( 

140 schema, centroid, "centroid", "pixel" 

141 ) 

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

143 if psfflux: 

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

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

146 if metadata: 

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

148 else: 

149 plugin = alg(control, name, schema) 

150 cat = afwTable.SourceCatalog(schema) 

151 if centroid: 

152 cat.defineCentroid(centroid) 

153 return plugin, cat 

154 

155 

156class ShapeTestCase(unittest.TestCase): 

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

158 

159 def setUp(self): 

160 

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

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(afwGeom.makeSkyWcs(crpix=geom.Point2D(1.0, 1.0), 

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

198 cdMatrix=cdMatrix)) 

199 

200 # load the corresponding test psf 

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

202 psfImg = afwImage.ImageD(psfFile) 

203 psfImg -= self.bkgd 

204 

205 kernel = afwMath.FixedKernel(psfImg) 

206 kernelPsf = algorithms.KernelPsf(kernel) 

207 exposure.setPsf(kernelPsf) 

208 

209 # perform the shape measurement 

210 msConfig = base.SingleFrameMeasurementConfig() 

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

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

213 msConfig.algorithms.names = [algorithmName] 

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

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

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

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

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

219 source = table.makeRecord() 

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

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

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

223 plugin.measure(source, exposure) 

224 

225 return source 

226 

227 def testHsmShape(self): 

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

229 

230 nFail = 0 

231 msg = "" 

232 

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

234 enumerate(file_indices)): 

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

236 

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

238 

239 ########################################## 

240 # see how we did 

241 if algName in ("KSB"): 

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

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

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

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

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

247 e1 = g1*scale 

248 e2 = g2*scale 

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

250 else: 

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

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

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

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

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

256 

257 tests = [ 

258 # label known-value measured tolerance 

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

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

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

262 

263 # sigma won't match exactly because 

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

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

266 ["shapeStatus", 0, flags, 0], 

267 ] 

268 

269 for test in tests: 

270 label, know, hsm, limit = test 

271 err = hsm - know 

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

273 label, know, hsm, err) 

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

275 msg += msgTmp 

276 nFail += 1 

277 

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

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

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

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

282 

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

284 

285 def testHsmSourceMoments(self): 

286 for (i, imageid) in enumerate(file_indices): 

287 source = self.runMeasurement("ext_shapeHSM_HsmSourceMoments", imageid, 

288 x_centroid[i], y_centroid[i], sky_var[i]) 

289 x = source.get("ext_shapeHSM_HsmSourceMoments_x") 

290 y = source.get("ext_shapeHSM_HsmSourceMoments_y") 

291 xx = source.get("ext_shapeHSM_HsmSourceMoments_xx") 

292 yy = source.get("ext_shapeHSM_HsmSourceMoments_yy") 

293 xy = source.get("ext_shapeHSM_HsmSourceMoments_xy") 

294 

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

296 offset = self.xy0 + self.offset 

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

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

299 

300 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius( 

301 moments_expected[i][1], moments_expected[i][2], moments_expected[i][0])) 

302 

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

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

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

306 

307 def testHsmSourceMomentsRound(self): 

308 for (i, imageid) in enumerate(file_indices): 

309 source = self.runMeasurement("ext_shapeHSM_HsmSourceMomentsRound", imageid, 

310 x_centroid[i], y_centroid[i], sky_var[i]) 

311 x = source.get("ext_shapeHSM_HsmSourceMomentsRound_x") 

312 y = source.get("ext_shapeHSM_HsmSourceMomentsRound_y") 

313 xx = source.get("ext_shapeHSM_HsmSourceMomentsRound_xx") 

314 yy = source.get("ext_shapeHSM_HsmSourceMomentsRound_yy") 

315 xy = source.get("ext_shapeHSM_HsmSourceMomentsRound_xy") 

316 flux = source.get("ext_shapeHSM_HsmSourceMomentsRound_Flux") 

317 

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

319 offset = self.xy0 + self.offset 

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

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

322 

323 expected = afwEll.Quadrupole(afwEll.SeparableDistortionDeterminantRadius( 

324 round_moments_expected[i][1], round_moments_expected[i][2], round_moments_expected[i][0])) 

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

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

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

328 

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

330 

331 

332class PyGaussianPsf(afwDetection.Psf): 

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

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

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

336 

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

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

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

340 self.sigma = sigma 

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

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

343 

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

345 bbox = self.computeBBox(position, color) 

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

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

348 rsqr = x**2 + y**2 

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

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

351 return img 

352 

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

354 bbox = self.computeBBox(position, color) 

355 if self.wrongBBox: 

356 # For DM-30426: 

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

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

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

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

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

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

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

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

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

366 rsqr = x**2 + y**2 

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

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

369 img.setXY0(geom.Point2I( 

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

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

372 )) 

373 return img 

374 

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

376 # Variable size bbox for addressing DM-29863 

377 dims = self.dimensions 

378 if self.varyBBox: 

379 if position.x > 20.0: 

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

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

382 

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

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

385 

386 

387class PsfMomentsTestCase(unittest.TestCase): 

388 """A test case for shape measurement""" 

389 

390 @lsst.utils.tests.methodParameters( 

391 # Make Cartesian product of settings to feed to methodParameters 

392 **dict(list(zip( 

393 (kwargs := dict( 

394 width=(2.0, 3.0, 4.0), 

395 useSourceCentroidOffset=(True, False), 

396 varyBBox=(True, False), 

397 wrongBBox=(True, False), 

398 center=( 

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

400 (23.5, 34.0), 

401 (23.5, 34.5), 

402 (23.15, 34.25), 

403 (22.81, 34.01), 

404 (22.81, 33.99), 

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

406 (-100.0, -100.0), 

407 (-100.5, -100.0), 

408 (-100.5, -100.5), 

409 ) 

410 )).keys(), 

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

412 ))) 

413 ) 

414 def testHsmPsfMoments( 

415 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

416 ): 

417 psf = PyGaussianPsf( 

418 35, 35, width, 

419 varyBBox=varyBBox, 

420 wrongBBox=wrongBBox 

421 ) 

422 exposure = afwImage.ExposureF(45, 56) 

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

424 exposure.setPsf(psf) 

425 

426 # perform the shape measurement 

427 msConfig = base.SingleFrameMeasurementConfig() 

428 msConfig.algorithms.names = ["ext_shapeHSM_HsmPsfMoments"] 

429 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsControl() 

430 self.assertFalse(control.useSourceCentroidOffset) 

431 control.useSourceCentroidOffset = useSourceCentroidOffset 

432 plugin, cat = makePluginAndCat( 

433 lsst.meas.extensions.shapeHSM.HsmPsfMomentsAlgorithm, 

434 "ext_shapeHSM_HsmPsfMoments", centroid="centroid", 

435 control=control) 

436 source = cat.addNew() 

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

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

439 offset = geom.Point2I(*center) 

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

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

442 plugin.measure(source, exposure) 

443 x = source.get("ext_shapeHSM_HsmPsfMoments_x") 

444 y = source.get("ext_shapeHSM_HsmPsfMoments_y") 

445 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx") 

446 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy") 

447 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy") 

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

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

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

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

452 

453 self.assertAlmostEqual(x, 0.0, 3) 

454 self.assertAlmostEqual(y, 0.0, 3) 

455 

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

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

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

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

460 

461 # Test schema documentation 

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

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

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

465 

466 @lsst.utils.tests.methodParameters( 

467 # Make Cartesian product of settings to feed to methodParameters 

468 **dict(list(zip( 

469 (kwargs := dict( 

470 width=(2.0, 3.0, 4.0), 

471 useSourceCentroidOffset=(True, False), 

472 varyBBox=(True, False), 

473 wrongBBox=(True, False), 

474 center=( 

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

476 (23.5, 34.0), 

477 (23.5, 34.5), 

478 (23.15, 34.25), 

479 (22.81, 34.01), 

480 (22.81, 33.99), 

481 ) 

482 )).keys(), 

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

484 ))) 

485 ) 

486 def testHsmPsfMomentsDebiased( 

487 self, width, useSourceCentroidOffset, varyBBox, wrongBBox, center 

488 ): 

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

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

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

492 # test similar to the biased moments above. 

493 var = 1.2 

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

495 # increases, so decrease tolerance. 

496 for flux, decimals in [ 

497 (1e6, 3), 

498 (1e4, 1), 

499 (1e3, 0), 

500 ]: 

501 psf = PyGaussianPsf( 

502 35, 35, width, 

503 varyBBox=varyBBox, 

504 wrongBBox=wrongBBox 

505 ) 

506 exposure = afwImage.ExposureF(45, 56) 

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

508 exposure.setPsf(psf) 

509 

510 # perform the shape measurement 

511 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl() 

512 self.assertTrue(control.useSourceCentroidOffset) 

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

514 control.useSourceCentroidOffset = useSourceCentroidOffset 

515 plugin, cat = makePluginAndCat( 

516 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

517 "ext_shapeHSM_HsmPsfMomentsDebiased", 

518 centroid="centroid", 

519 psfflux="base_PsfFlux", 

520 control=control 

521 ) 

522 source = cat.addNew() 

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

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

525 offset = geom.Point2I(*center) 

526 source.set("base_PsfFlux_instFlux", flux) 

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

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

529 

530 plugin.measure(source, exposure) 

531 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

532 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

533 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

534 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

535 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

536 for flag in [ 

537 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

538 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

539 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

540 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

541 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

542 ]: 

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

544 

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

546 

547 self.assertAlmostEqual(x, 0.0, decimals) 

548 self.assertAlmostEqual(y, 0.0, decimals) 

549 

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

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

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

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

554 

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

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

557 exposure2 = afwImage.ExposureF(45, 56) 

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

559 # ignoring it 

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

561 exposure2.setPsf(psf) 

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

563 

564 control2 = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl() 

565 control2.noiseSource = "meta" 

566 control2.useSourceCentroidOffset = useSourceCentroidOffset 

567 plugin2, cat2 = makePluginAndCat( 

568 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

569 "ext_shapeHSM_HsmPsfMomentsDebiased", 

570 centroid="centroid", 

571 psfflux="base_PsfFlux", 

572 control=control2 

573 ) 

574 source2 = cat2.addNew() 

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

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

577 offset2 = geom.Point2I(*center) 

578 source2.set("base_PsfFlux_instFlux", flux) 

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

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

581 

582 plugin2.measure(source2, exposure2) 

583 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

584 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

585 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

586 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

587 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

588 for flag in [ 

589 "ext_shapeHSM_HsmPsfMomentsDebiased_flag", 

590 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels", 

591 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained", 

592 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source", 

593 "ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge" 

594 ]: 

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

596 

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

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

599 # plane is a c++ float. 

600 self.assertAlmostEqual(x, x2, 8) 

601 self.assertAlmostEqual(y, y2, 8) 

602 self.assertAlmostEqual(xx, xx2, 5) 

603 self.assertAlmostEqual(xy, xy2, 5) 

604 self.assertAlmostEqual(yy, yy2, 5) 

605 

606 # Test schema documentation 

607 for fieldName in cat.schema.extract("*HsmPsfMomentsDebiased_[xy]*"): 

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

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

610 

611 testHsmPsfMomentsDebiasedEdgeArgs = dict( 

612 width=(2.0, 3.0, 4.0), 

613 useSourceCentroidOffset=(True, False), 

614 center=( 

615 (1.2, 1.3), 

616 (33.2, 50.1) 

617 ) 

618 ) 

619 

620 @lsst.utils.tests.methodParameters( 

621 # Make Cartesian product of settings to feed to methodParameters 

622 **dict(list(zip( 

623 (kwargs := dict( 

624 width=(2.0, 3.0, 4.0), 

625 useSourceCentroidOffset=(True, False), 

626 center=[ 

627 (1.2, 1.3), 

628 (33.2, 50.1) 

629 ] 

630 )).keys(), 

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

632 ))) 

633 ) 

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

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

636 # increases, so decrease tolerance. 

637 var = 1.2 

638 for flux, decimals in [ 

639 (1e6, 3), 

640 (1e4, 2), 

641 (1e3, 1), 

642 ]: 

643 psf = PyGaussianPsf(35, 35, width) 

644 exposure = afwImage.ExposureF(45, 56) 

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

646 exposure.setPsf(psf) 

647 

648 # perform the shape measurement 

649 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl() 

650 control.useSourceCentroidOffset = useSourceCentroidOffset 

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

652 plugin, cat = makePluginAndCat( 

653 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

654 "ext_shapeHSM_HsmPsfMomentsDebiased", 

655 centroid="centroid", 

656 psfflux="base_PsfFlux", 

657 control=control 

658 ) 

659 source = cat.addNew() 

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

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

662 offset = geom.Point2I(*center) 

663 source.set("base_PsfFlux_instFlux", flux) 

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

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

666 

667 # Edge fails when setting noise from var plane 

668 with self.assertRaises(base.MeasurementError): 

669 plugin.measure(source, exposure) 

670 

671 # Succeeds when noise is from meta 

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

673 control.noiseSource = "meta" 

674 plugin, cat = makePluginAndCat( 

675 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

676 "ext_shapeHSM_HsmPsfMomentsDebiased", 

677 centroid="centroid", 

678 psfflux="base_PsfFlux", 

679 control=control 

680 ) 

681 source = cat.addNew() 

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

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

684 offset = geom.Point2I(*center) 

685 source.set("base_PsfFlux_instFlux", flux) 

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

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

688 plugin.measure(source, exposure) 

689 

690 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

691 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

692 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

693 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

694 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

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

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

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

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

699 # but _does_ set EDGE flag in this case 

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

701 

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

703 

704 self.assertAlmostEqual(x, 0.0, decimals) 

705 self.assertAlmostEqual(y, 0.0, decimals) 

706 

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

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

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

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

711 

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

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

714 plugin, cat = makePluginAndCat( 

715 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

716 "ext_shapeHSM_HsmPsfMomentsDebiased", 

717 centroid="centroid", 

718 psfflux="base_PsfFlux", 

719 control=control 

720 ) 

721 source = cat.addNew() 

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

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

724 offset = geom.Point2I(*center) 

725 source.set("base_PsfFlux_instFlux", flux) 

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

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

728 with self.assertRaises(base.FatalAlgorithmError): 

729 plugin.measure(source, exposure) 

730 

731 def testHsmPsfMomentsDebiasedBadNoiseSource(self): 

732 control = lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedControl() 

733 control.noiseSource = "ACM" 

734 with self.assertRaises(base.MeasurementError): 

735 makePluginAndCat( 

736 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

737 "ext_shapeHSM_HsmPsfMomentsDebiased", 

738 centroid="centroid", 

739 control=control 

740 ) 

741 

742 

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

744 pass 

745 

746 

747def setup_module(module): 

748 lsst.utils.tests.init() 

749 

750 

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

752 lsst.utils.tests.init() 

753 unittest.main()