Coverage for tests/test_measureApCorr.py: 16%

175 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-30 03:08 -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.assertRaisesRegex(measureApCorr.MeasureApCorrError, 

157 "Unable to measure aperture correction for 'test1Ap'"): 

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

159 

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

161 # allowed to fail. This should run without raising an exception, but 

162 # will log a warning. 

163 for name in self.names: 

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

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

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

167 self.assertIn("Unable to measure aperture correction for 'test1Ap'", cm.output[0]) 

168 

169 def testSourceNotUsed(self): 

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

171 sourceCat = self.makeCatalog() 

172 source = sourceCat.addNew() 

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

174 source[nameAp + "_instFlux"] = 5.1 

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

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

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

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

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

180 apCorrFlagName = "apcorr_" + nameAp + "_used" 

181 

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

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

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

185 # Check that the final source is not used. 

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

187 

188 def testSourceUsed(self): 

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

190 sourceCat = self.makeCatalog() 

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

192 for name in self.names: 

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

194 

195 def testApertureMeasOnes(self): 

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

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

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

199 sourceCat = self.makeCatalog() 

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

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

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

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

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

205 

206 def testApertureMeasTens(self): 

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

208 apCorr_factor = 10. 

209 sourceCat = self.makeCatalog(apCorrScale=apCorr_factor) 

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

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

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

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

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

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

216 

217 def testFilterBadValue(self): 

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

219 sourceCat = self.makeCatalog() 

220 source = sourceCat.addNew() 

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

222 source[nameAp + "_instFlux"] = 100.0 

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

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

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

226 source[self.unresolvedName] = 0.0 

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

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

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

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

231 

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

233 

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

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

236 for name in self.names: 

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

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

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

240 # Check that the final source is not used. 

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

242 

243 def testTooFewSourcesAfterFiltering(self): 

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

245 sourceCat = self.makeCatalog() 

246 self.meas_apCorr_task.config.minDegreesOfFreedom = 4 

247 

248 for name in self.names: 

249 nameAp = name + self.apNameStr 

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

251 

252 with self.assertRaisesRegex(measureApCorr.MeasureApCorrError, 

253 f"Unable to measure aperture correction for '{nameAp}'"): 

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

255 

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

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

258 for name in self.names: 

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

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

261 

262 

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

264 pass 

265 

266 

267def setup_module(module): 

268 lsst.utils.tests.init() 

269 

270 

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

272 lsst.utils.tests.init() 

273 unittest.main()