Coverage for tests/test_measureApCorr.py: 16%

172 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-13 02:27 -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 unittest 

24import numpy as np 

25 

26import lsst.geom 

27import lsst.afw.image as afwImage 

28import lsst.afw.table as afwTable 

29from lsst.afw.math import ChebyshevBoundedField 

30import lsst.pex.config 

31import lsst.meas.algorithms.measureApCorr as measureApCorr 

32from lsst.meas.base.apCorrRegistry import addApCorrName 

33import lsst.meas.base.tests 

34import lsst.utils.tests 

35 

36 

37def apCorrDefaultMap(value=None, bbox=None): 

38 default_coefficients = np.ones((1, 1), dtype=float) 

39 default_coefficients /= value 

40 default_apCorrMap = ChebyshevBoundedField(bbox, default_coefficients) 

41 default_fill = afwImage.ImageF(bbox) 

42 default_apCorrMap.fillImage(default_fill) 

43 return(default_fill) 

44 

45 

46class MeasureApCorrTestCase(lsst.meas.base.tests.AlgorithmTestCase, lsst.utils.tests.TestCase): 

47 

48 def makeCatalog(self, apCorrScale=1.0, numSources=5): 

49 sourceCat = afwTable.SourceCatalog(self.schema) 

50 

51 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"]) 

52 x = np.random.rand(numSources)*self.exposure.getWidth() + self.exposure.getX0() 

53 y = np.random.rand(numSources)*self.exposure.getHeight() + self.exposure.getY0() 

54 for _i in range(numSources): 

55 source_test_centroid = lsst.geom.Point2D(x[_i], y[_i]) 

56 source = sourceCat.addNew() 

57 source.set(centroidKey, source_test_centroid) 

58 # All sources are unresolved. 

59 source[self.unresolvedName] = 0.0 

60 

61 source_test_instFlux = 5.1 

62 source_test_instFluxErr = 1e-3 

63 

64 for name in self.names: 

65 sourceCat[name + "_instFlux"] = source_test_instFlux 

66 sourceCat[name + "_instFluxErr"] = source_test_instFluxErr 

67 sourceCat[name + "_flag"] = np.zeros(len(sourceCat), dtype=bool) 

68 sourceCat[name + self.apNameStr + "_instFlux"] = source_test_instFlux * apCorrScale 

69 sourceCat[name + self.apNameStr + "_instFluxErr"] = source_test_instFluxErr * apCorrScale 

70 sourceCat[name + self.apNameStr + "_flag"] = np.zeros(len(sourceCat), dtype=bool) 

71 

72 return(sourceCat) 

73 

74 def setUp(self): 

75 schema = afwTable.SourceTable.makeMinimalSchema() 

76 apNameStr = "Ap" 

77 config = measureApCorr.MeasureApCorrTask.ConfigClass() 

78 unresolvedName = config.sourceSelector.active.unresolved.name 

79 # Add fields in anti-sorted order to try to impose a need for sorting 

80 # in the addition of the apCorr fields (may happen by fluke, but this 

81 # is the best we can do to test this here. 

82 names = ["test2", "test1"] 

83 for name in names: 

84 apName = name + apNameStr 

85 addApCorrName(apName) 

86 schema.addField(name + "_instFlux", type=float) 

87 schema.addField(name + "_instFluxErr", type=float) 

88 schema.addField(name + "_flag", type="Flag") 

89 schema.addField(apName + "_instFlux", type=float) 

90 schema.addField(apName + "_instFluxErr", type=float) 

91 schema.addField(apName + "_flag", type="Flag") 

92 schema.addField(names[0] + "_Centroid_x", type=float) 

93 schema.addField(names[0] + "_Centroid_y", type=float) 

94 schema.getAliasMap().set("slot_Centroid", names[0] + "_Centroid") 

95 schema.addField(unresolvedName, type=float) 

96 schema.addField("deblend_nChild", type=np.int32) 

97 for flag in [ 

98 "base_PixelFlags_flag_edge", 

99 "base_PixelFlags_flag_interpolatedCenter", 

100 "base_PixelFlags_flag_saturatedCenter", 

101 "base_PixelFlags_flag_crCenter", 

102 "base_PixelFlags_flag_bad", 

103 "base_PixelFlags_flag_interpolated", 

104 "base_PixelFlags_flag_saturated", 

105 ]: 

106 schema.addField(flag, type="Flag") 

107 config.refFluxName = names[0] 

108 config.sourceSelector["science"].signalToNoise.fluxField = names[0] + "_instFlux" 

109 config.sourceSelector["science"].signalToNoise.errField = names[0] + "_instFluxErr" 

110 self.meas_apCorr_task = measureApCorr.MeasureApCorrTask(schema=schema, config=config) 

111 self.names = names 

112 self.apNameStr = apNameStr 

113 self.schema = schema 

114 self.exposure = lsst.afw.image.ExposureF(10, 10) 

115 self.unresolvedName = unresolvedName 

116 

117 def tearDown(self): 

118 del self.schema 

119 del self.meas_apCorr_task 

120 del self.exposure 

121 

122 def testAddFields(self): 

123 """Instantiating the task should add one field to the schema.""" 

124 for name in self.names: 

125 self.assertIn("apcorr_" + name + self.apNameStr + "_used", self.schema.getNames()) 

126 sortedNames = sorted(self.names) 

127 key0 = self.schema.find("apcorr_" + sortedNames[0] + self.apNameStr + "_used").key 

128 key1 = self.schema.find("apcorr_" + sortedNames[1] + self.apNameStr + "_used").key 

129 # Check that the apCorr fields were added in a sorted order (not 

130 # foolproof as this could have happened by fluke, but it's the best 

131 # we can do to test this here (having added the two fields in an anti- 

132 # sorted order). 

133 self.assertLess(key0.getOffset() + key0.getBit(), key1.getOffset() + key1.getBit()) 

134 

135 def testReturnApCorrMap(self): 

136 """The measureApCorr task should return a structure with a single key 'apCorrMap'.""" 

137 struct = self.meas_apCorr_task.run(catalog=self.makeCatalog(), exposure=self.exposure) 

138 self.assertEqual(list(struct.getDict().keys()), ['apCorrMap']) 

139 

140 def testApCorrMapKeys(self): 

141 """An apCorrMap structure should have two keys per name supplied to addApCorrName().""" 

142 key_names = [] 

143 for name in self.names: 

144 apFluxName = name + self.apNameStr + "_instFlux" 

145 apFluxErrName = name + self.apNameStr + "_instFluxErr" 

146 struct = self.meas_apCorr_task.run(catalog=self.makeCatalog(), exposure=self.exposure) 

147 key_names.append(apFluxName) 

148 key_names.append(apFluxErrName) 

149 self.assertEqual(set(struct.apCorrMap.keys()), set(key_names)) 

150 

151 def testTooFewSources(self): 

152 """ If there are too few sources, check that an exception is raised.""" 

153 # Create an empty catalog with no sources to process. 

154 catalog = afwTable.SourceCatalog(self.schema) 

155 with self.assertRaisesRegex(measureApCorr.MeasureApCorrError, "failed on required algorithm"): 

156 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure) 

157 

158 # We now try again after declaring that the aperture correction is 

159 # allowed to fail. This should run cleanly without raising an exception. 

160 for name in self.names: 

161 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr) 

162 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure) 

163 

164 def testSourceNotUsed(self): 

165 """ Check that a source outside the bounding box is flagged as not used (False).""" 

166 sourceCat = self.makeCatalog() 

167 source = sourceCat.addNew() 

168 nameAp = self.names[0] + "Ap" 

169 source[nameAp + "_instFlux"] = 5.1 

170 source[nameAp + "_instFluxErr"] = 1e-3 

171 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1 

172 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3 

173 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"]) 

174 source.set(centroidKey, lsst.geom.Point2D(15, 7.1)) 

175 apCorrFlagName = "apcorr_" + nameAp + "_used" 

176 

177 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

178 # Check that all but the final source are used. 

179 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all()) 

180 # Check that the final source is not used. 

181 self.assertFalse(sourceCat[apCorrFlagName][-1]) 

182 

183 def testSourceUsed(self): 

184 """Check that valid sources inside the bounding box that are used have their flags set to True.""" 

185 sourceCat = self.makeCatalog() 

186 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

187 for name in self.names: 

188 self.assertTrue(sourceCat["apcorr_" + name + self.apNameStr + "_used"].all()) 

189 

190 def testApertureMeasOnes(self): 

191 """ Check that sources with aperture fluxes exactly the same as their catalog fluxes 

192 returns an aperture correction map of 1s""" 

193 apFluxName = self.names[0] + self.apNameStr + "_instFlux" 

194 sourceCat = self.makeCatalog() 

195 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

196 default_fill = apCorrDefaultMap(value=1.0, bbox=self.exposure.getBBox()) 

197 test_fill = afwImage.ImageF(self.exposure.getBBox()) 

198 struct.apCorrMap[apFluxName].fillImage(test_fill) 

199 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray()) 

200 

201 def testApertureMeasTens(self): 

202 """Check that aperture correction scales source fluxes in the correct direction.""" 

203 apCorr_factor = 10. 

204 sourceCat = self.makeCatalog(apCorrScale=apCorr_factor) 

205 apFluxName = self.names[0] + self.apNameStr + "_instFlux" 

206 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

207 default_fill = apCorrDefaultMap(value=apCorr_factor, bbox=self.exposure.getBBox()) 

208 test_fill = afwImage.ImageF(self.exposure.getBBox()) 

209 struct.apCorrMap[apFluxName].fillImage(test_fill) 

210 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray()) 

211 

212 def testFilterBadValue(self): 

213 """Check that the aperture correction filters a bad value.""" 

214 sourceCat = self.makeCatalog() 

215 source = sourceCat.addNew() 

216 nameAp = self.names[0] + self.apNameStr 

217 source[nameAp + "_instFlux"] = 100.0 

218 source[nameAp + "_instFluxErr"] = 1e-3 

219 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1 

220 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3 

221 source[self.unresolvedName] = 0.0 

222 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"]) 

223 x = self.exposure.getX0() + 1 

224 y = self.exposure.getY0() + 1 

225 source.set(centroidKey, lsst.geom.Point2D(x, y)) 

226 

227 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

228 

229 # Check that both Ap fluxes are removed as outliers; one is due 

230 # to being unfilled (nan), the other is a large outlier. 

231 for name in self.names: 

232 apCorrFlagName = "apcorr_" + name + self.apNameStr + "_used" 

233 # Check that all but the final source are used. 

234 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all()) 

235 # Check that the final source is not used. 

236 self.assertFalse(sourceCat[apCorrFlagName][-1]) 

237 

238 def testTooFewSourcesAfterFiltering(self): 

239 """Check that the aperture correction fails when too many are filtered.""" 

240 sourceCat = self.makeCatalog() 

241 self.meas_apCorr_task.config.minDegreesOfFreedom = 4 

242 

243 for name in self.names: 

244 nameAp = name + self.apNameStr 

245 sourceCat[nameAp + "_instFlux"][0] = 100.0 

246 

247 with self.assertRaisesRegex(RuntimeError, "only 4 sources remain"): 

248 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

249 

250 # We now try again after declaring that the aperture correction is 

251 # allowed to fail. This should run cleanly without raising an exception. 

252 for name in self.names: 

253 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr) 

254 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure) 

255 

256 

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

258 pass 

259 

260 

261def setup_module(module): 

262 lsst.utils.tests.init() 

263 

264 

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

266 lsst.utils.tests.init() 

267 unittest.main()