Coverage for tests/test_ApertureFlux.py: 18%

155 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 12:41 +0000

1# This file is part of meas_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import unittest 

23 

24import numpy as np 

25 

26import lsst.geom 

27import lsst.afw.geom 

28import lsst.afw.image 

29import lsst.utils.tests 

30from lsst.meas.base import ApertureFluxAlgorithm 

31from lsst.meas.base.tests import (AlgorithmTestCase, FluxTransformTestCase, 

32 SingleFramePluginTransformSetupHelper) 

33 

34 

35class ApertureFluxTestCase(lsst.utils.tests.TestCase): 

36 """Test case for the ApertureFlux algorithm base class. 

37 """ 

38 

39 def setUp(self): 

40 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(20, -100), lsst.geom.Point2I(100, -20)) 

41 self.exposure = lsst.afw.image.ExposureF(self.bbox) 

42 self.exposure.getMaskedImage().getImage().set(1.0) 

43 self.exposure.getMaskedImage().getVariance().set(0.25) 

44 self.ctrl = ApertureFluxAlgorithm.Control() 

45 

46 def tearDown(self): 

47 del self.bbox 

48 del self.exposure 

49 

50 def computeNaiveArea(self, position, radius): 

51 """Computes the area of a circular aperture. 

52 

53 Calculates the area of the aperture by the "naive" approach of testing 

54 each pixel to see whether its center lies within the aperture. 

55 """ 

56 x, y = np.meshgrid(np.arange(self.bbox.getBeginX(), self.bbox.getEndX()), 

57 np.arange(self.bbox.getBeginY(), self.bbox.getEndY())) 

58 return ((x - position.getX())**2 + (y - position.getY())**2 <= radius**2).sum() 

59 

60 def testNaive(self): 

61 positions = [lsst.geom.Point2D(60.0, -60.0), 

62 lsst.geom.Point2D(60.5, -60.0), 

63 lsst.geom.Point2D(60.0, -60.5), 

64 lsst.geom.Point2D(60.5, -60.5)] 

65 radii = [12.0, 17.0] 

66 for position in positions: 

67 for radius in radii: 

68 ellipse = lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(radius, radius, 0.0), position) 

69 area = self.computeNaiveArea(position, radius) 

70 # test that this isn't the same as the sinc instFlux 

71 self.assertFloatsNotEqual( 

72 ApertureFluxAlgorithm.computeSincFlux(self.exposure.getMaskedImage().getImage(), 

73 ellipse, self.ctrl).instFlux, area) 

74 

75 def check(method, image): 

76 """Test that all instFlux measurement invocations work. 

77 

78 That is, that they return the expected value. 

79 """ 

80 result = method(image, ellipse, self.ctrl) 

81 self.assertFloatsAlmostEqual(result.instFlux, area) 

82 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number)) 

83 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number)) 

84 if hasattr(image, "getVariance"): 

85 self.assertFloatsAlmostEqual(result.instFluxErr, (area*0.25)**0.5) 

86 else: 

87 self.assertTrue(np.isnan(result.instFluxErr)) 

88 check(ApertureFluxAlgorithm.computeNaiveFlux, self.exposure.getMaskedImage()) 

89 check(ApertureFluxAlgorithm.computeNaiveFlux, self.exposure.getMaskedImage().getImage()) 

90 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage()) 

91 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage().getImage()) 

92 # test failure conditions when the aperture itself is truncated 

93 invalid = ApertureFluxAlgorithm.computeNaiveFlux( 

94 self.exposure.getMaskedImage().getImage(), 

95 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(12.0, 12.0), 

96 lsst.geom.Point2D(25.0, -60.0)), 

97 self.ctrl) 

98 self.assertTrue(invalid.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number)) 

99 self.assertFalse(invalid.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number)) 

100 self.assertTrue(np.isnan(invalid.instFlux)) 

101 

102 def testSinc(self): 

103 positions = [lsst.geom.Point2D(60.0, -60.0), 

104 lsst.geom.Point2D(60.5, -60.0), 

105 lsst.geom.Point2D(60.0, -60.5), 

106 lsst.geom.Point2D(60.5, -60.5)] 

107 radii = [7.0, 9.0] 

108 for position in positions: 

109 for radius in radii: 

110 ellipse = lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(radius, radius, 0.0), position) 

111 area = ellipse.getCore().getArea() 

112 # test that this isn't the same as the naive instFlux 

113 self.assertFloatsNotEqual( 

114 ApertureFluxAlgorithm.computeNaiveFlux(self.exposure.getMaskedImage().getImage(), 

115 ellipse, self.ctrl).instFlux, area) 

116 

117 def check(method, image): 

118 # test that all the ways we could invoke sinc flux 

119 # measurement produce the expected result 

120 result = method(image, ellipse, self.ctrl) 

121 self.assertFloatsAlmostEqual(result.instFlux, area, rtol=1E-3) 

122 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number)) 

123 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number)) 

124 if hasattr(image, "getVariance"): 

125 self.assertFalse(np.isnan(result.instFluxErr)) 

126 else: 

127 self.assertTrue(np.isnan(result.instFluxErr)) 

128 check(ApertureFluxAlgorithm.computeSincFlux, self.exposure.getMaskedImage()) 

129 check(ApertureFluxAlgorithm.computeSincFlux, self.exposure.getMaskedImage().getImage()) 

130 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage()) 

131 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage().getImage()) 

132 # test failure conditions when the aperture itself is truncated 

133 invalid1 = ApertureFluxAlgorithm.computeSincFlux( 

134 self.exposure.getMaskedImage().getImage(), 

135 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(9.0, 9.0), lsst.geom.Point2D(25.0, -60.0)), 

136 self.ctrl) 

137 self.assertTrue(invalid1.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number)) 

138 self.assertTrue(invalid1.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number)) 

139 self.assertTrue(np.isnan(invalid1.instFlux)) 

140 # test failure conditions when the aperture is not truncated, but the 

141 # sinc coeffs are 

142 invalid2 = ApertureFluxAlgorithm.computeSincFlux( 

143 self.exposure.getMaskedImage().getImage(), 

144 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(9.0, 9.0), lsst.geom.Point2D(30.0, -60.0)), 

145 self.ctrl) 

146 self.assertFalse(invalid2.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number)) 

147 self.assertTrue(invalid2.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number)) 

148 self.assertFalse(np.isnan(invalid2.instFlux)) 

149 

150 

151class CircularApertureFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

152 """Test case for the CircularApertureFlux algorithm/plugin. 

153 """ 

154 

155 def setUp(self): 

156 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

157 lsst.geom.Extent2I(100, 100)) 

158 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox) 

159 # first source is a point 

160 self.dataset.addSource(100000.0, lsst.geom.Point2D(49.5, 49.5)) 

161 

162 def tearDown(self): 

163 del self.bbox 

164 del self.dataset 

165 

166 def testSingleFramePlugin(self): 

167 baseName = "base_CircularApertureFlux" 

168 config = self.makeSingleFrameMeasurementConfig(baseName) 

169 config.plugins[baseName].maxSincRadius = 20 

170 ctrl = config.plugins[baseName].makeControl() 

171 algMetadata = lsst.daf.base.PropertyList() 

172 task = self.makeSingleFrameMeasurementTask(config=config, algMetadata=algMetadata) 

173 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=0) 

174 task.run(catalog, exposure) 

175 radii = algMetadata.getArray("%s_RADII" % (baseName.upper(),)) 

176 self.assertEqual(list(radii), list(ctrl.radii)) 

177 for record in catalog: 

178 lastFlux = 0.0 

179 lastFluxErr = 0.0 

180 for n, radius in enumerate(radii): 

181 # Test that the flags are what we expect 

182 prefix = ApertureFluxAlgorithm.makeFieldPrefix(baseName, radius) 

183 if radius <= ctrl.maxSincRadius: 

184 self.assertFalse(record.get(record.schema.join(prefix, "flag"))) 

185 self.assertFalse(record.get(record.schema.join(prefix, "flag_apertureTruncated"))) 

186 self.assertEqual( 

187 record.get(record.schema.join(prefix, "flag_sincCoeffsTruncated")), 

188 radius > 12 

189 ) 

190 else: 

191 self.assertTrue(record.schema.join(prefix, "flag_sincCoeffsTruncated") 

192 not in record.getSchema()) 

193 self.assertEqual(record.get(record.schema.join(prefix, "flag")), radius >= 50) 

194 self.assertEqual(record.get(record.schema.join(prefix, "flag_apertureTruncated")), 

195 radius >= 50) 

196 # Test that the instFluxes and uncertainties increase as we 

197 # increase the apertures, or that they match the true instFlux 

198 # within 3 sigma. This is just a test as to whether the 

199 # values are reasonable. As to whether the values are exactly 

200 # correct, we rely on the tests on ApertureFluxAlgorithm's 

201 # static methods, as the way the plugins code calls that is 

202 # extremely simple, so if the results we get are reasonable, 

203 # it's hard to imagine how they could be incorrect if 

204 # ApertureFluxAlgorithm's tests are valid. 

205 currentFlux = record.get(record.schema.join(prefix, "instFlux")) 

206 currentFluxErr = record.get(record.schema.join(prefix, "instFluxErr")) 

207 if not record.get(record.schema.join(prefix, "flag")): 

208 self.assertTrue(currentFlux > lastFlux 

209 or (record.get("truth_instFlux") - currentFlux) < 3*currentFluxErr) 

210 self.assertGreater(currentFluxErr, lastFluxErr) 

211 lastFlux = currentFlux 

212 lastFluxErr = currentFluxErr 

213 else: 

214 self.assertTrue(np.isnan(currentFlux)) 

215 self.assertTrue(np.isnan(currentFluxErr)) 

216 # When measuring an isolated point source with a sufficiently 

217 # large aperture, we should recover the known input instFlux. 

218 if record.get("truth_isStar") and record.get("parent") == 0: 

219 self.assertFloatsAlmostEqual(record.get("base_CircularApertureFlux_25_0_instFlux"), 

220 record.get("truth_instFlux"), rtol=0.02) 

221 

222 def testForcedPlugin(self): 

223 baseName = "base_CircularApertureFlux" 

224 algMetadata = lsst.daf.base.PropertyList() 

225 task = self.makeForcedMeasurementTask(baseName, algMetadata=algMetadata) 

226 radii = algMetadata.getArray("%s_RADII" % (baseName.upper(),)) 

227 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=1) 

228 measDataset = self.dataset.transform(measWcs) 

229 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=1) 

230 refCat = self.dataset.catalog 

231 refWcs = self.dataset.exposure.getWcs() 

232 measCat = task.generateMeasCat(exposure, refCat, refWcs) 

233 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs) 

234 task.run(measCat, exposure, refCat, refWcs) 

235 for measRecord, truthRecord in zip(measCat, truthCatalog): 

236 # Centroid tolerances set to ~ single precision epsilon 

237 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_x"), 

238 truthRecord.get("truth_x"), rtol=1E-7) 

239 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_y"), 

240 truthRecord.get("truth_y"), rtol=1E-7) 

241 for n, radius in enumerate(radii): 

242 prefix = ApertureFluxAlgorithm.makeFieldPrefix(baseName, radius) 

243 self.assertFalse(measRecord.get(measRecord.schema.join(prefix, "flag"))) 

244 # CircularApertureFlux isn't designed to do a good job in 

245 # forced mode, because it doesn't account for changes in the 

246 # PSF or changes in the WCS. Hence, this is really just a 

247 # test to make sure the values are reasonable and that it runs 

248 # with no unexpected errors. 

249 self.assertFloatsAlmostEqual(measRecord.get(measRecord.schema.join(prefix, "instFlux")), 

250 truthCatalog.get("truth_instFlux"), rtol=1.0) 

251 self.assertLess(measRecord.get(measRecord.schema.join(prefix, "instFluxErr")), (n+1)*150.0) 

252 

253 

254class ApertureFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper, 

255 lsst.utils.tests.TestCase): 

256 

257 class CircApFluxAlgorithmFactory: 

258 """Supply an empty ``PropertyList`` to `CircularApertureFluxAlgorithm`. 

259 

260 This is a helper class to make testing more convenient. 

261 """ 

262 

263 def __call__(self, control, name, inputSchema): 

264 return lsst.meas.base.CircularApertureFluxAlgorithm(control, name, inputSchema, 

265 lsst.daf.base.PropertyList()) 

266 

267 controlClass = lsst.meas.base.ApertureFluxAlgorithm.Control 

268 algorithmClass = CircApFluxAlgorithmFactory() 

269 transformClass = lsst.meas.base.ApertureFluxTransform 

270 flagNames = ('flag', 'flag_apertureTruncated', 'flag_sincCoeffsTruncated') 

271 singleFramePlugins = ('base_CircularApertureFlux',) 

272 forcedPlugins = ('base_CircularApertureFlux',) 

273 

274 def testTransform(self): 

275 """Test `ApertureFluxTransform` with a synthetic catalog. 

276 """ 

277 FluxTransformTestCase.testTransform(self, [ApertureFluxAlgorithm.makeFieldPrefix(self.name, r) 

278 for r in self.control.radii]) 

279 

280 

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

282 pass 

283 

284 

285def setup_module(module): 

286 lsst.utils.tests.init() 

287 

288 

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

290 lsst.utils.tests.init() 

291 unittest.main()