Coverage for tests/test_doubleShapeletPsfApprox.py: 16%

261 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-11 03:14 -0800

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 

27import warnings 

28 

29import lsst.utils.tests 

30import lsst.afw.detection 

31import lsst.afw.image 

32import lsst.geom 

33import lsst.afw.geom 

34import lsst.afw.geom.ellipses 

35import lsst.log 

36import lsst.utils.logging 

37import lsst.meas.modelfit 

38import lsst.meas.algorithms 

39 

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

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

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

43 

44 

45class DoubleShapeletPsfApproxTestMixin: 

46 

47 Algorithm = lsst.meas.modelfit.DoubleShapeletPsfApproxAlgorithm 

48 

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

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

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

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

53 self.psf = psf 

54 self.atol = atol 

55 if ctrl is None: 

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

57 self.ctrl = ctrl 

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

59 setattr(self.ctrl, name, value) 

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

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

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

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

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

65 self.exposure.setWcs(wcs) 

66 self.exposure.setPsf(self.psf) 

67 

68 def tearDown(self): 

69 del self.exposure 

70 del self.psf 

71 del self.ctrl 

72 del self.atol 

73 

74 def setupTaskConfig(self, config): 

75 config.slots.shape = None 

76 config.slots.psfFlux = None 

77 config.slots.apFlux = None 

78 config.slots.gaussianFlux = None 

79 config.slots.modelFlux = None 

80 config.slots.calibFlux = None 

81 config.doReplaceWithNoise = False 

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

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

84 

85 def checkBounds(self, msf): 

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

87 

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

89 """ 

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

91 self.assertEqual( 

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

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

94 ) 

95 self.assertEqual( 

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

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

98 ) 

99 self.assertGreater( 

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

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

102 ) 

103 self.assertGreater( 

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

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

106 ) 

107 self.assertLess( 

108 self.ctrl.minRadius, 

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

110 ) 

111 self.assertLess( 

112 self.ctrl.minRadius, 

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

114 ) 

115 self.assertLess( 

116 self.ctrl.minRadiusDiff, 

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

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

119 ) 

120 

121 def checkRatios(self, msf): 

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

123 

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

125 in later stages of the fit. 

126 """ 

127 inner = msf.getComponents()[0] 

128 outer = msf.getComponents()[1] 

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

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

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

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

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

134 self.assertFloatsAlmostEqual( 

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

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

137 ) 

138 

139 def makeImages(self, msf): 

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

141 """ 

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

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

144 msf.evaluate().addToImage(modelImage) 

145 return dataImage, modelImage 

146 

147 def checkFitQuality(self, msf): 

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

149 """ 

150 dataImage, modelImage = self.makeImages(msf) 

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

152 plotOnFailure=True) 

153 

154 def testSingleFramePlugin(self): 

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

156 """ 

157 with warnings.catch_warnings(): 

158 warnings.filterwarnings("ignore", message="ignoreSlotPluginChecks", category=FutureWarning) 

159 config = lsst.meas.base.SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=True) 

160 self.setupTaskConfig(config) 

161 config.slots.centroid = "centroid" 

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

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

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

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

166 measRecord = measCat.addNew() 

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

168 task.run(measCat, self.exposure) 

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

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

171 msf = measRecord.get(key) 

172 self.checkBounds(msf) 

173 self.checkFitQuality(msf) 

174 

175 def testForcedPlugin(self): 

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

177 """ 

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

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

180 self.setupTaskConfig(config) 

181 config.slots.centroid = "base_TransformedCentroid" 

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

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

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

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

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

187 refRecord = refCat.addNew() 

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

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

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

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

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

193 measRecord = measCat[0] 

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

195 measSchema = measCat.schema 

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

197 msf = measRecord.get(key) 

198 self.checkBounds(msf) 

199 self.checkFitQuality(msf) 

200 

201 def testInitializeResult(self): 

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

203 with the right peakRatio and radiusRatio. 

204 """ 

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

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

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

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

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

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

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

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

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

214 self.checkRatios(msf) 

215 

216 def testFitMoments(self): 

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

218 correctly. 

219 """ 

220 MOMENTS_RTOL = 1E-13 

221 image = self.psf.computeKernelImage() 

222 array = image.getArray() 

223 bbox = image.getBBox() 

224 x, y = numpy.meshgrid( 

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

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

227 ) 

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

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

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

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

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

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

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

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

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

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

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

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

240 rtol=MOMENTS_RTOL) 

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

242 self.checkRatios(msf) 

243 self.checkBounds(msf) 

244 

245 def testObjective(self): 

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

247 """ 

248 image = self.psf.computeKernelImage() 

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

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

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

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

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

254 image, model = self.makeImages(msf) 

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

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

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

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

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

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

261 objective.computeResiduals(parameters, residuals) 

262 self.assertFloatsAlmostEqual( 

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

264 image.getArray() - model.getArray() 

265 ) 

266 step = 1E-6 

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

268 objective.differentiateResiduals(parameters, derivatives) 

269 for i in range(parameters.size): 

270 original = parameters[i] 

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

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

273 parameters[i] = original + step 

274 objective.computeResiduals(parameters, r1) 

275 parameters[i] = original - step 

276 objective.computeResiduals(parameters, r2) 

277 parameters[i] = original 

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

279 self.assertFloatsAlmostEqual( 

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

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

282 atol=1E-11 

283 ) 

284 

285 def testFitProfile(self): 

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

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

288 """ 

289 image = self.psf.computeKernelImage() 

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

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

292 prev = lsst.shapelet.MultiShapeletFunction(msf) 

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

294 

295 def getEllipticity(m, c): 

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

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

298 ) 

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

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

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

302 

303 def computeChiSq(m): 

304 data, model = self.makeImages(m) 

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

306 bestChiSq = computeChiSq(msf) 

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

308 step = 1E-4 

309 for component in msf.getComponents(): 

310 # 0th-order amplitude perturbation 

311 original = component.getCoefficients()[0] 

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

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

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

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

316 component.getCoefficients()[0] = original 

317 # Radius perturbation 

318 original = component.getEllipse() 

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

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

321 component.setEllipse(original) 

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

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

324 component.setEllipse(original) 

325 

326 def testFitShapelets(self): 

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

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

329 do not improve the fit. 

330 """ 

331 image = self.psf.computeKernelImage() 

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

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

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

335 prev = lsst.shapelet.MultiShapeletFunction(msf) 

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

337 self.assertFloatsAlmostEqual( 

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

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

340 ) 

341 self.assertFloatsAlmostEqual( 

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

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

344 ) 

345 

346 def computeChiSq(m): 

347 data, model = self.makeImages(m) 

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

349 bestChiSq = computeChiSq(msf) 

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

351 step = 1E-4 

352 for component in msf.getComponents(): 

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

354 original = component.getCoefficients()[i] 

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

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

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

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

359 component.getCoefficients()[i] = original 

360 

361 def testSingleFrameConfigIO(self): 

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

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

364 self.setupTaskConfig(config1) 

365 stream = StringIO() 

366 config1.saveToStream(stream) 

367 config2.loadFromStream(stream.getvalue()) 

368 self.assertEqual(config1, config2) 

369 

370 

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

372 

373 def setUp(self): 

374 numpy.random.seed(500) 

375 DoubleShapeletPsfApproxTestMixin.initialize( 

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

377 innerOrder=0, outerOrder=0, peakRatio=0.0 

378 ) 

379 

380 

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

382 

383 def setUp(self): 

384 numpy.random.seed(500) 

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

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

387 DoubleShapeletPsfApproxTestMixin.initialize( 

388 self, psf=image, 

389 innerOrder=3, outerOrder=2, 

390 atol=0.0005 

391 ) 

392 

393 

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

395 

396 def setUp(self): 

397 numpy.random.seed(500) 

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

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

400 DoubleShapeletPsfApproxTestMixin.initialize( 

401 self, psf=image, 

402 innerOrder=2, outerOrder=1, 

403 atol=0.002 

404 ) 

405 

406 

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

408 pass 

409 

410 

411def setup_module(module): 

412 lsst.utils.tests.init() 

413 

414 

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

416 lsst.utils.tests.init() 

417 unittest.main()