Coverage for tests/test_doubleShapeletPsfApprox.py: 19%

258 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 02:24 -0700

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2016 AURA/LSST. 

5# 

6# This product includes software developed by the 

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

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 LSST License Statement and 

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

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

22# 

23import os 

24import unittest 

25import numpy 

26from io import StringIO 

27 

28import lsst.utils.tests 

29import lsst.afw.detection 

30import lsst.afw.image 

31import lsst.geom 

32import lsst.afw.geom 

33import lsst.afw.geom.ellipses 

34import lsst.log 

35import lsst.utils.logging 

36import lsst.meas.modelfit 

37import lsst.meas.algorithms 

38 

39# Set trace to 0-5 to view debug messages. Level 5 enables all traces. 

40lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.Optimizer", -1) 

41lsst.utils.logging.trace_set_at("lsst.meas.modelfit.optimizer.solveTrustRegion", -1) 

42 

43 

44class DoubleShapeletPsfApproxTestMixin: 

45 

46 Algorithm = lsst.meas.modelfit.DoubleShapeletPsfApproxAlgorithm 

47 

48 def initialize(self, psf, ctrl=None, atol=1E-4, **kwds): 

49 if not isinstance(psf, lsst.afw.detection.Psf): 

50 kernel = lsst.afw.math.FixedKernel(psf) 

51 psf = lsst.meas.algorithms.KernelPsf(kernel) 

52 self.psf = psf 

53 self.atol = atol 

54 if ctrl is None: 

55 ctrl = lsst.meas.modelfit.DoubleShapeletPsfApproxControl() 

56 self.ctrl = ctrl 

57 for name, value in kwds.items(): 

58 setattr(self.ctrl, name, value) 

59 self.exposure = lsst.afw.image.ExposureF(1, 1) 

60 scale = 5.0e-5 * lsst.geom.degrees 

61 wcs = lsst.afw.geom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 0.0), 

62 crval=lsst.geom.SpherePoint(45, 45, lsst.geom.degrees), 

63 cdMatrix=lsst.afw.geom.makeCdMatrix(scale=scale)) 

64 self.exposure.setWcs(wcs) 

65 self.exposure.setPsf(self.psf) 

66 

67 def tearDown(self): 

68 del self.exposure 

69 del self.psf 

70 del self.ctrl 

71 del self.atol 

72 

73 def setupTaskConfig(self, config): 

74 config.slots.shape = None 

75 config.slots.psfFlux = None 

76 config.slots.apFlux = None 

77 config.slots.gaussianFlux = None 

78 config.slots.modelFlux = None 

79 config.slots.calibFlux = None 

80 config.doReplaceWithNoise = False 

81 config.plugins.names = ["modelfit_DoubleShapeletPsfApprox"] 

82 config.plugins["modelfit_DoubleShapeletPsfApprox"].readControl(self.ctrl) 

83 

84 def checkBounds(self, msf): 

85 """Check that the bounds specified in the control object are met by a MultiShapeletFunction. 

86 

87 These requirements must be true after a call to any fit method or measure(). 

88 """ 

89 self.assertEqual(len(msf.getComponents()), 2) 

90 self.assertEqual( 

91 lsst.shapelet.computeSize(self.ctrl.innerOrder), 

92 len(msf.getComponents()[0].getCoefficients()) 

93 ) 

94 self.assertEqual( 

95 lsst.shapelet.computeSize(self.ctrl.outerOrder), 

96 len(msf.getComponents()[1].getCoefficients()) 

97 ) 

98 self.assertGreater( 

99 self.ctrl.maxRadiusBoxFraction * (self.psf.computeKernelImage().getBBox().getArea())**0.5, 

100 lsst.afw.geom.ellipses.Axes(msf.getComponents()[0].getEllipse().getCore()).getA() 

101 ) 

102 self.assertGreater( 

103 self.ctrl.maxRadiusBoxFraction * (self.psf.computeKernelImage().getBBox().getArea())**0.5, 

104 lsst.afw.geom.ellipses.Axes(msf.getComponents()[1].getEllipse().getCore()).getA() 

105 ) 

106 self.assertLess( 

107 self.ctrl.minRadius, 

108 lsst.afw.geom.ellipses.Axes(msf.getComponents()[0].getEllipse().getCore()).getB() 

109 ) 

110 self.assertLess( 

111 self.ctrl.minRadius, 

112 lsst.afw.geom.ellipses.Axes(msf.getComponents()[1].getEllipse().getCore()).getB() 

113 ) 

114 self.assertLess( 

115 self.ctrl.minRadiusDiff, 

116 (msf.getComponents()[1].getEllipse().getCore().getDeterminantRadius() 

117 - msf.getComponents()[0].getEllipse().getCore().getDeterminantRadius()) 

118 ) 

119 

120 def checkRatios(self, msf): 

121 """Check that the ratios specified in the control object are met by a MultiShapeletFunction. 

122 

123 These requirements must be true after initializeResult and fitMoments, but will are relaxed 

124 in later stages of the fit. 

125 """ 

126 inner = msf.getComponents()[0] 

127 outer = msf.getComponents()[1] 

128 position = msf.getComponents()[0].getEllipse().getCenter() 

129 self.assertFloatsAlmostEqual(position.getX(), msf.getComponents()[1].getEllipse().getCenter().getX()) 

130 self.assertFloatsAlmostEqual(position.getY(), msf.getComponents()[1].getEllipse().getCenter().getY()) 

131 self.assertFloatsAlmostEqual(outer.evaluate()(position), 

132 inner.evaluate()(position)*self.ctrl.peakRatio) 

133 self.assertFloatsAlmostEqual( 

134 outer.getEllipse().getCore().getDeterminantRadius(), 

135 inner.getEllipse().getCore().getDeterminantRadius() * self.ctrl.radiusRatio 

136 ) 

137 

138 def makeImages(self, msf): 

139 """Return an Image of the data and an Image of the model for comparison. 

140 """ 

141 dataImage = self.exposure.getPsf().computeKernelImage() 

142 modelImage = dataImage.Factory(dataImage.getBBox()) 

143 msf.evaluate().addToImage(modelImage) 

144 return dataImage, modelImage 

145 

146 def checkFitQuality(self, msf): 

147 """Check the quality of the fit by comparing to the PSF image. 

148 """ 

149 dataImage, modelImage = self.makeImages(msf) 

150 self.assertFloatsAlmostEqual(dataImage.getArray(), modelImage.getArray(), atol=self.atol, 

151 plotOnFailure=True) 

152 

153 def testSingleFramePlugin(self): 

154 """Run the algorithm as a single-frame plugin and check the quality of the fit. 

155 """ 

156 config = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass() 

157 self.setupTaskConfig(config) 

158 config.slots.centroid = "centroid" 

159 schema = lsst.afw.table.SourceTable.makeMinimalSchema() 

160 centroidKey = lsst.afw.table.Point2DKey.addFields(schema, "centroid", "centroid", "pixel") 

161 task = lsst.meas.base.SingleFrameMeasurementTask(config=config, schema=schema) 

162 measCat = lsst.afw.table.SourceCatalog(schema) 

163 measRecord = measCat.addNew() 

164 measRecord.set(centroidKey, lsst.geom.Point2D(0.0, 0.0)) 

165 task.run(measCat, self.exposure) 

166 self.assertFalse(measRecord.get("modelfit_DoubleShapeletPsfApprox_flag")) 

167 key = lsst.shapelet.MultiShapeletFunctionKey(schema["modelfit"]["DoubleShapeletPsfApprox"]) 

168 msf = measRecord.get(key) 

169 self.checkBounds(msf) 

170 self.checkFitQuality(msf) 

171 

172 def testForcedPlugin(self): 

173 """Run the algorithm as a forced plugin and check the quality of the fit. 

174 """ 

175 config = lsst.meas.base.ForcedMeasurementTask.ConfigClass() 

176 config.copyColumns = {"id": "objectId", "parent": "parentObjectId"} 

177 self.setupTaskConfig(config) 

178 config.slots.centroid = "base_TransformedCentroid" 

179 config.plugins.names |= ["base_TransformedCentroid"] 

180 refSchema = lsst.afw.table.SourceTable.makeMinimalSchema() 

181 refCentroidKey = lsst.afw.table.Point2DKey.addFields(refSchema, "centroid", "centroid", "pixel") 

182 refSchema.getAliasMap().set("slot_Centroid", "centroid") 

183 refCat = lsst.afw.table.SourceCatalog(refSchema) 

184 refRecord = refCat.addNew() 

185 refRecord.set(refCentroidKey, lsst.geom.Point2D(0.0, 0.0)) 

186 refWcs = self.exposure.getWcs() # same as measurement Wcs 

187 task = lsst.meas.base.ForcedMeasurementTask(config=config, refSchema=refSchema) 

188 measCat = task.generateMeasCat(self.exposure, refCat, refWcs) 

189 task.run(measCat, self.exposure, refCat, refWcs) 

190 measRecord = measCat[0] 

191 self.assertFalse(measRecord.get("modelfit_DoubleShapeletPsfApprox_flag")) 

192 measSchema = measCat.schema 

193 key = lsst.shapelet.MultiShapeletFunctionKey(measSchema["modelfit"]["DoubleShapeletPsfApprox"]) 

194 msf = measRecord.get(key) 

195 self.checkBounds(msf) 

196 self.checkFitQuality(msf) 

197 

198 def testInitializeResult(self): 

199 """Test that initializeResult() returns a unit-flux, unit-circle MultiShapeletFunction 

200 with the right peakRatio and radiusRatio. 

201 """ 

202 msf = self.Algorithm.initializeResult(self.ctrl) 

203 self.assertFloatsAlmostEqual(msf.evaluate().integrate(), 1.0) 

204 moments = msf.evaluate().computeMoments() 

205 axes = lsst.afw.geom.ellipses.Axes(moments.getCore()) 

206 self.assertFloatsAlmostEqual(moments.getCenter().getX(), 0.0) 

207 self.assertFloatsAlmostEqual(moments.getCenter().getY(), 0.0) 

208 self.assertFloatsAlmostEqual(axes.getA(), 1.0) 

209 self.assertFloatsAlmostEqual(axes.getB(), 1.0) 

210 self.assertEqual(len(msf.getComponents()), 2) 

211 self.checkRatios(msf) 

212 

213 def testFitMoments(self): 

214 """Test that fitMoments() preserves peakRatio and radiusRatio while setting moments 

215 correctly. 

216 """ 

217 MOMENTS_RTOL = 1E-13 

218 image = self.psf.computeKernelImage() 

219 array = image.getArray() 

220 bbox = image.getBBox() 

221 x, y = numpy.meshgrid( 

222 numpy.arange(bbox.getBeginX(), bbox.getEndX()), 

223 numpy.arange(bbox.getBeginY(), bbox.getEndY()) 

224 ) 

225 msf = self.Algorithm.initializeResult(self.ctrl) 

226 self.Algorithm.fitMoments(msf, self.ctrl, image) 

227 self.assertFloatsAlmostEqual(msf.evaluate().integrate(), array.sum(), rtol=MOMENTS_RTOL) 

228 moments = msf.evaluate().computeMoments() 

229 q = lsst.afw.geom.ellipses.Quadrupole(moments.getCore()) 

230 cx = (x*array).sum()/array.sum() 

231 cy = (y*array).sum()/array.sum() 

232 self.assertFloatsAlmostEqual(moments.getCenter().getX(), cx, rtol=MOMENTS_RTOL) 

233 self.assertFloatsAlmostEqual(moments.getCenter().getY(), cy, rtol=MOMENTS_RTOL) 

234 self.assertFloatsAlmostEqual(q.getIxx(), ((x - cx)**2 * array).sum()/array.sum(), rtol=MOMENTS_RTOL) 

235 self.assertFloatsAlmostEqual(q.getIyy(), ((y - cy)**2 * array).sum()/array.sum(), rtol=MOMENTS_RTOL) 

236 self.assertFloatsAlmostEqual(q.getIxy(), ((x - cx)*(y - cy)*array).sum()/array.sum(), 

237 rtol=MOMENTS_RTOL) 

238 self.assertEqual(len(msf.getComponents()), 2) 

239 self.checkRatios(msf) 

240 self.checkBounds(msf) 

241 

242 def testObjective(self): 

243 """Test that model evaluation agrees with derivative evaluation in the objective object. 

244 """ 

245 image = self.psf.computeKernelImage() 

246 msf = self.Algorithm.initializeResult(self.ctrl) 

247 self.Algorithm.fitMoments(msf, self.ctrl, image) 

248 moments = msf.evaluate().computeMoments() 

249 r0 = moments.getCore().getDeterminantRadius() 

250 objective = self.Algorithm.makeObjective(moments, self.ctrl, image) 

251 image, model = self.makeImages(msf) 

252 parameters = numpy.zeros(4, dtype=float) 

253 parameters[0] = msf.getComponents()[0].getCoefficients()[0] 

254 parameters[1] = msf.getComponents()[1].getCoefficients()[0] 

255 parameters[2] = msf.getComponents()[0].getEllipse().getCore().getDeterminantRadius() / r0 

256 parameters[3] = msf.getComponents()[1].getEllipse().getCore().getDeterminantRadius() / r0 

257 residuals = numpy.zeros(image.getArray().size, dtype=float) 

258 objective.computeResiduals(parameters, residuals) 

259 self.assertFloatsAlmostEqual( 

260 residuals.reshape(image.getHeight(), image.getWidth()), 

261 image.getArray() - model.getArray() 

262 ) 

263 step = 1E-6 

264 derivatives = numpy.zeros((parameters.size, residuals.size), dtype=float).transpose() 

265 objective.differentiateResiduals(parameters, derivatives) 

266 for i in range(parameters.size): 

267 original = parameters[i] 

268 r1 = numpy.zeros(residuals.size, dtype=float) 

269 r2 = numpy.zeros(residuals.size, dtype=float) 

270 parameters[i] = original + step 

271 objective.computeResiduals(parameters, r1) 

272 parameters[i] = original - step 

273 objective.computeResiduals(parameters, r2) 

274 parameters[i] = original 

275 d = (r1 - r2)/(2.0*step) 

276 self.assertFloatsAlmostEqual( 

277 d.reshape(image.getHeight(), image.getWidth()), 

278 derivatives[:, i].reshape(image.getHeight(), image.getWidth()), 

279 atol=1E-11 

280 ) 

281 

282 def testFitProfile(self): 

283 """Test that fitProfile() does not modify the ellipticity, that it improves the fit, and 

284 that small perturbations to the zeroth-order amplitudes and radii do not improve the fit. 

285 """ 

286 image = self.psf.computeKernelImage() 

287 msf = self.Algorithm.initializeResult(self.ctrl) 

288 self.Algorithm.fitMoments(msf, self.ctrl, image) 

289 prev = lsst.shapelet.MultiShapeletFunction(msf) 

290 self.Algorithm.fitProfile(msf, self.ctrl, image) 

291 

292 def getEllipticity(m, c): 

293 s = lsst.afw.geom.ellipses.SeparableDistortionDeterminantRadius( 

294 m.getComponents()[c].getEllipse().getCore() 

295 ) 

296 return numpy.array([s.getE1(), s.getE2()]) 

297 self.assertFloatsAlmostEqual(getEllipticity(prev, 0), getEllipticity(msf, 0), rtol=1E-13) 

298 self.assertFloatsAlmostEqual(getEllipticity(prev, 1), getEllipticity(msf, 1), rtol=1E-13) 

299 

300 def computeChiSq(m): 

301 data, model = self.makeImages(m) 

302 return numpy.sum((data.getArray() - model.getArray())**2) 

303 bestChiSq = computeChiSq(msf) 

304 self.assertLessEqual(bestChiSq, computeChiSq(prev)) 

305 step = 1E-4 

306 for component in msf.getComponents(): 

307 # 0th-order amplitude perturbation 

308 original = component.getCoefficients()[0] 

309 component.getCoefficients()[0] = original + step 

310 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

311 component.getCoefficients()[0] = original - step 

312 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

313 component.getCoefficients()[0] = original 

314 # Radius perturbation 

315 original = component.getEllipse() 

316 component.getEllipse().getCore().scale(1.0 + step) 

317 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

318 component.setEllipse(original) 

319 component.getEllipse().getCore().scale(1.0 - step) 

320 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

321 component.setEllipse(original) 

322 

323 def testFitShapelets(self): 

324 """Test that fitShapelets() does not modify the zeroth order coefficients or ellipse, 

325 that it improves the fit, and that small perturbations to the higher-order coefficients 

326 do not improve the fit. 

327 """ 

328 image = self.psf.computeKernelImage() 

329 msf = self.Algorithm.initializeResult(self.ctrl) 

330 self.Algorithm.fitMoments(msf, self.ctrl, image) 

331 self.Algorithm.fitProfile(msf, self.ctrl, image) 

332 prev = lsst.shapelet.MultiShapeletFunction(msf) 

333 self.Algorithm.fitShapelets(msf, self.ctrl, image) 

334 self.assertFloatsAlmostEqual( 

335 prev.getComponents()[0].getEllipse().getParameterVector(), 

336 msf.getComponents()[0].getEllipse().getParameterVector() 

337 ) 

338 self.assertFloatsAlmostEqual( 

339 prev.getComponents()[1].getEllipse().getParameterVector(), 

340 msf.getComponents()[1].getEllipse().getParameterVector() 

341 ) 

342 

343 def computeChiSq(m): 

344 data, model = self.makeImages(m) 

345 return numpy.sum((data.getArray() - model.getArray())**2) 

346 bestChiSq = computeChiSq(msf) 

347 self.assertLessEqual(bestChiSq, computeChiSq(prev)) 

348 step = 1E-4 

349 for component in msf.getComponents(): 

350 for i in range(1, len(component.getCoefficients())): 

351 original = component.getCoefficients()[i] 

352 component.getCoefficients()[i] = original + step 

353 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

354 component.getCoefficients()[i] = original - step 

355 self.assertLessEqual(bestChiSq, computeChiSq(msf)) 

356 component.getCoefficients()[i] = original 

357 

358 def testSingleFrameConfigIO(self): 

359 config1 = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass() 

360 config2 = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass() 

361 self.setupTaskConfig(config1) 

362 stream = StringIO() 

363 config1.saveToStream(stream) 

364 config2.loadFromStream(stream.getvalue()) 

365 self.assertEqual(config1, config2) 

366 

367 

368class SingleGaussianTestCase(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase): 

369 

370 def setUp(self): 

371 numpy.random.seed(500) 

372 DoubleShapeletPsfApproxTestMixin.initialize( 

373 self, psf=lsst.afw.detection.GaussianPsf(25, 25, 2.0), 

374 innerOrder=0, outerOrder=0, peakRatio=0.0 

375 ) 

376 

377 

378class HigherOrderTestCase0(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase): 

379 

380 def setUp(self): 

381 numpy.random.seed(500) 

382 image = lsst.afw.image.ImageD(os.path.join(os.path.dirname(os.path.realpath(__file__)), 

383 "data", "psfs/great3-0.fits")) 

384 DoubleShapeletPsfApproxTestMixin.initialize( 

385 self, psf=image, 

386 innerOrder=3, outerOrder=2, 

387 atol=0.0005 

388 ) 

389 

390 

391class HigherOrderTestCase1(DoubleShapeletPsfApproxTestMixin, lsst.utils.tests.TestCase): 

392 

393 def setUp(self): 

394 numpy.random.seed(500) 

395 image = lsst.afw.image.ImageD(os.path.join(os.path.dirname(os.path.realpath(__file__)), 

396 "data", "psfs/great3-1.fits")) 

397 DoubleShapeletPsfApproxTestMixin.initialize( 

398 self, psf=image, 

399 innerOrder=2, outerOrder=1, 

400 atol=0.002 

401 ) 

402 

403 

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

405 pass 

406 

407 

408def setup_module(module): 

409 lsst.utils.tests.init() 

410 

411 

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

413 lsst.utils.tests.init() 

414 unittest.main()