Coverage for tests/test_measureApCorr.py: 15%

177 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 03:05 -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 

25import logging 

26 

27import lsst.geom 

28import lsst.afw.image as afwImage 

29import lsst.afw.table as afwTable 

30from lsst.afw.math import ChebyshevBoundedField 

31import lsst.pex.config 

32import lsst.meas.algorithms.measureApCorr as measureApCorr 

33from lsst.meas.base.apCorrRegistry import addApCorrName 

34import lsst.meas.base.tests 

35import lsst.utils.tests 

36 

37 

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

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

40 default_coefficients /= value 

41 default_apCorrMap = ChebyshevBoundedField(bbox, default_coefficients) 

42 default_fill = afwImage.ImageF(bbox) 

43 default_apCorrMap.fillImage(default_fill) 

44 return(default_fill) 

45 

46 

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

48 

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

50 sourceCat = afwTable.SourceCatalog(self.schema) 

51 

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

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

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

55 for _i in range(numSources): 

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

57 source = sourceCat.addNew() 

58 source.set(centroidKey, source_test_centroid) 

59 # All sources are unresolved. 

60 source[self.unresolvedName] = 0.0 

61 

62 source_test_instFlux = 5.1 

63 source_test_instFluxErr = 1e-3 

64 

65 for name in self.names: 

66 sourceCat[name + "_instFlux"] = source_test_instFlux 

67 sourceCat[name + "_instFluxErr"] = source_test_instFluxErr 

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

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

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

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

72 

73 return(sourceCat) 

74 

75 def setUp(self): 

76 schema = afwTable.SourceTable.makeMinimalSchema() 

77 apNameStr = "Ap" 

78 config = measureApCorr.MeasureApCorrTask.ConfigClass() 

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

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

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

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

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

84 for name in names: 

85 apName = name + apNameStr 

86 addApCorrName(apName) 

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

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

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

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

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

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

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

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

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

96 schema.addField(unresolvedName, type=float) 

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

98 for flag in [ 

99 "base_PixelFlags_flag_edge", 

100 "base_PixelFlags_flag_interpolatedCenter", 

101 "base_PixelFlags_flag_saturatedCenter", 

102 "base_PixelFlags_flag_crCenter", 

103 "base_PixelFlags_flag_bad", 

104 "base_PixelFlags_flag_interpolated", 

105 "base_PixelFlags_flag_saturated", 

106 ]: 

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

108 config.refFluxName = names[0] 

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

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

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

112 self.names = names 

113 self.apNameStr = apNameStr 

114 self.schema = schema 

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

116 self.unresolvedName = unresolvedName 

117 

118 def tearDown(self): 

119 del self.schema 

120 del self.meas_apCorr_task 

121 del self.exposure 

122 

123 def testAddFields(self): 

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

125 for name in self.names: 

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

127 sortedNames = sorted(self.names) 

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

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

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

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

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

133 # sorted order). 

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

135 

136 def testReturnApCorrMap(self): 

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

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

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

140 

141 def testApCorrMapKeys(self): 

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

143 key_names = [] 

144 for name in self.names: 

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

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

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

148 key_names.append(apFluxName) 

149 key_names.append(apFluxErrName) 

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

151 

152 def testTooFewSources(self): 

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

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

155 catalog = afwTable.SourceCatalog(self.schema) 

156 with self.assertLogs(level=logging.WARNING) as cm: 

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

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

159 self.assertIn("Unable to measure aperture correction for required algorithm", cm.output[0]) 

160 

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

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

163 for name in self.names: 

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

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

166 

167 def testSourceNotUsed(self): 

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

169 sourceCat = self.makeCatalog() 

170 source = sourceCat.addNew() 

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

172 source[nameAp + "_instFlux"] = 5.1 

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

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

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

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

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

178 apCorrFlagName = "apcorr_" + nameAp + "_used" 

179 

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

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

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

183 # Check that the final source is not used. 

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

185 

186 def testSourceUsed(self): 

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

188 sourceCat = self.makeCatalog() 

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

190 for name in self.names: 

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

192 

193 def testApertureMeasOnes(self): 

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

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

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

197 sourceCat = self.makeCatalog() 

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

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

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

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

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

203 

204 def testApertureMeasTens(self): 

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

206 apCorr_factor = 10. 

207 sourceCat = self.makeCatalog(apCorrScale=apCorr_factor) 

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

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

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

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

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

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

214 

215 def testFilterBadValue(self): 

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

217 sourceCat = self.makeCatalog() 

218 source = sourceCat.addNew() 

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

220 source[nameAp + "_instFlux"] = 100.0 

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

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

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

224 source[self.unresolvedName] = 0.0 

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

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

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

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

229 

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

231 

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

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

234 for name in self.names: 

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

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

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

238 # Check that the final source is not used. 

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

240 

241 def testTooFewSourcesAfterFiltering(self): 

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

243 sourceCat = self.makeCatalog() 

244 self.meas_apCorr_task.config.minDegreesOfFreedom = 4 

245 

246 for name in self.names: 

247 nameAp = name + self.apNameStr 

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

249 

250 with self.assertLogs(level=logging.WARNING) as cm: 

251 with self.assertRaisesRegex(measureApCorr.MeasureApCorrError, "Aperture correction failed"): 

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

253 self.assertIn("only 4 sources remain", cm.output[0]) 

254 

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

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

257 for name in self.names: 

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

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

260 

261 

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

263 pass 

264 

265 

266def setup_module(module): 

267 lsst.utils.tests.init() 

268 

269 

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

271 lsst.utils.tests.init() 

272 unittest.main()