Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

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 

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

344 bbox = self.computeBBox(position, color) 

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

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

347 rsqr = x**2 + y**2 

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

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

350 return img 

351 

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

353 bbox = self.computeBBox(position, color) 

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

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

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

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

358 rsqr = x**2 + y**2 

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

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

361 img.setXY0(geom.Point2I( 

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

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

364 )) 

365 return img 

366 

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

368 # Variable size bbox for addressing DM-29863 

369 dims = self.dimensions 

370 if self.varyBBox: 

371 if position.x > 20.0: 

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

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

374 

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

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

377 

378 

379class PsfMomentsTestCase(unittest.TestCase): 

380 """A test case for shape measurement""" 

381 

382 def testHsmPsfMoments(self): 

383 for width in (2.0, 3.0, 4.0): 

384 for useSourceCentroidOffset in [True, False]: 

385 for center in [ 

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

387 (23.5, 34.0), 

388 (23.5, 34.5), 

389 (23.15, 34.25), 

390 (22.81, 34.01), 

391 (22.81, 33.99), 

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

393 (-100.0, -100.0), 

394 (-100.5, -100.0), 

395 (-100.5, -100.5), 

396 ]: 

397 psf = PyGaussianPsf(35, 35, width, varyBBox=True) 

398 exposure = afwImage.ExposureF(45, 56) 

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

400 exposure.setPsf(psf) 

401 

402 # perform the shape measurement 

403 msConfig = base.SingleFrameMeasurementConfig() 

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

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

406 self.assertFalse(control.useSourceCentroidOffset) 

407 control.useSourceCentroidOffset = useSourceCentroidOffset 

408 plugin, cat = makePluginAndCat( 

409 lsst.meas.extensions.shapeHSM.HsmPsfMomentsAlgorithm, 

410 "ext_shapeHSM_HsmPsfMoments", centroid="centroid", 

411 control=control) 

412 source = cat.addNew() 

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

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

415 offset = geom.Point2I(*center) 

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

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

418 plugin.measure(source, exposure) 

419 x = source.get("ext_shapeHSM_HsmPsfMoments_x") 

420 y = source.get("ext_shapeHSM_HsmPsfMoments_y") 

421 xx = source.get("ext_shapeHSM_HsmPsfMoments_xx") 

422 yy = source.get("ext_shapeHSM_HsmPsfMoments_yy") 

423 xy = source.get("ext_shapeHSM_HsmPsfMoments_xy") 

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

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

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

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

428 

429 self.assertAlmostEqual(x, 0.0, 3) 

430 self.assertAlmostEqual(y, 0.0, 3) 

431 

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

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

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

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

436 

437 def testHsmPsfMomentsDebiased(self): 

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

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

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

441 # test similar to the biased moments above. 

442 var = 1.2 

443 for width in (2.0, 3.0, 4.0): 

444 for useSourceCentroidOffset in [True, False]: 

445 for center in [ 

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

447 (23.5, 34.0), 

448 (23.5, 34.5), 

449 (23.15, 34.25), 

450 (22.81, 34.01), 

451 (22.81, 33.99) 

452 ]: 

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

454 # increases, so decrease tolerance. 

455 for flux, decimals in [ 

456 (1e6, 3), 

457 (1e4, 2), 

458 (1e3, 1), 

459 ]: 

460 psf = PyGaussianPsf(35, 35, width) 

461 exposure = afwImage.ExposureF(45, 56) 

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

463 exposure.setPsf(psf) 

464 

465 # perform the shape measurement 

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

467 self.assertTrue(control.useSourceCentroidOffset) 

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

469 control.useSourceCentroidOffset = useSourceCentroidOffset 

470 plugin, cat = makePluginAndCat( 

471 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

472 "ext_shapeHSM_HsmPsfMomentsDebiased", 

473 centroid="centroid", 

474 psfflux="base_PsfFlux", 

475 control=control 

476 ) 

477 source = cat.addNew() 

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

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

480 offset = geom.Point2I(*center) 

481 source.set("base_PsfFlux_instFlux", flux) 

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

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

484 

485 plugin.measure(source, exposure) 

486 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

487 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

488 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

489 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

490 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

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

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

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

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

495 self.assertFalse(source.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge")) 

496 

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

498 

499 self.assertAlmostEqual(x, 0.0, decimals) 

500 self.assertAlmostEqual(y, 0.0, decimals) 

501 

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

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

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

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

506 

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

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

509 exposure2 = afwImage.ExposureF(45, 56) 

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

511 # ignoring it 

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

513 exposure2.setPsf(psf) 

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

515 

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

517 control2.noiseSource = "meta" 

518 control2.useSourceCentroidOffset = useSourceCentroidOffset 

519 plugin2, cat2 = makePluginAndCat( 

520 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

521 "ext_shapeHSM_HsmPsfMomentsDebiased", 

522 centroid="centroid", 

523 psfflux="base_PsfFlux", 

524 control=control2 

525 ) 

526 source2 = cat2.addNew() 

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

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

529 offset2 = geom.Point2I(*center) 

530 source2.set("base_PsfFlux_instFlux", flux) 

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

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

533 

534 plugin2.measure(source2, exposure2) 

535 x2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

536 y2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

537 xx2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

538 yy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

539 xy2 = source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

540 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag")) 

541 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_no_pixels")) 

542 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_not_contained")) 

543 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_parent_source")) 

544 self.assertFalse(source2.get("ext_shapeHSM_HsmPsfMomentsDebiased_flag_edge")) 

545 

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

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

548 # plane is a c++ float. 

549 self.assertAlmostEqual(x, x2, 8) 

550 self.assertAlmostEqual(y, y2, 8) 

551 self.assertAlmostEqual(xx, xx2, 6) 

552 self.assertAlmostEqual(xy, xy2, 6) 

553 self.assertAlmostEqual(yy, yy2, 6) 

554 

555 def testHsmPsfMomentsDebiasedEdge(self): 

556 var = 1.2 

557 for width in (2.0, 3.0, 4.0): 

558 for useSourceCentroidOffset in [True, False]: 

559 for center in [ 

560 (1.2, 1.3), 

561 (33.2, 50.1) 

562 ]: 

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

564 # increases, so decrease tolerance. 

565 for flux, decimals in [ 

566 (1e6, 3), 

567 (1e4, 2), 

568 (1e3, 1), 

569 ]: 

570 psf = PyGaussianPsf(35, 35, width) 

571 exposure = afwImage.ExposureF(45, 56) 

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

573 exposure.setPsf(psf) 

574 

575 # perform the shape measurement 

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

577 control.useSourceCentroidOffset = useSourceCentroidOffset 

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

579 plugin, cat = makePluginAndCat( 

580 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

581 "ext_shapeHSM_HsmPsfMomentsDebiased", 

582 centroid="centroid", 

583 psfflux="base_PsfFlux", 

584 control=control 

585 ) 

586 source = cat.addNew() 

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

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

589 offset = geom.Point2I(*center) 

590 source.set("base_PsfFlux_instFlux", flux) 

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

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

593 

594 # Edge fails when setting noise from var plane 

595 with self.assertRaises(base.MeasurementError): 

596 plugin.measure(source, exposure) 

597 

598 # Succeeds when noise is from meta 

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

600 control.noiseSource = "meta" 

601 plugin, cat = makePluginAndCat( 

602 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

603 "ext_shapeHSM_HsmPsfMomentsDebiased", 

604 centroid="centroid", 

605 psfflux="base_PsfFlux", 

606 control=control 

607 ) 

608 source = cat.addNew() 

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

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

611 offset = geom.Point2I(*center) 

612 source.set("base_PsfFlux_instFlux", flux) 

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

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

615 plugin.measure(source, exposure) 

616 

617 x = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_x") 

618 y = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_y") 

619 xx = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xx") 

620 yy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_yy") 

621 xy = source.get("ext_shapeHSM_HsmPsfMomentsDebiased_xy") 

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

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

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

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

626 # but _does_ set EDGE flag in this case 

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

628 

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

630 

631 self.assertAlmostEqual(x, 0.0, decimals) 

632 self.assertAlmostEqual(y, 0.0, decimals) 

633 

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

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

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

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

638 

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

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

641 plugin, cat = makePluginAndCat( 

642 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

643 "ext_shapeHSM_HsmPsfMomentsDebiased", 

644 centroid="centroid", 

645 psfflux="base_PsfFlux", 

646 control=control 

647 ) 

648 source = cat.addNew() 

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

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

651 offset = geom.Point2I(*center) 

652 source.set("base_PsfFlux_instFlux", flux) 

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

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

655 with self.assertRaises(base.FatalAlgorithmError): 

656 plugin.measure(source, exposure) 

657 

658 def testHsmPsfMomentsDebiasedBadNoiseSource(self): 

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

660 control.noiseSource = "ACM" 

661 with self.assertRaises(base.MeasurementError): 

662 makePluginAndCat( 

663 lsst.meas.extensions.shapeHSM.HsmPsfMomentsDebiasedAlgorithm, 

664 "ext_shapeHSM_HsmPsfMomentsDebiased", 

665 centroid="centroid", 

666 control=control 

667 ) 

668 

669 

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

671 pass 

672 

673 

674def setup_module(module): 

675 lsst.utils.tests.init() 

676 

677 

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

679 lsst.utils.tests.init() 

680 unittest.main()