Coverage for tests/test_InputCount.py: 16%

148 statements  

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

22"""Tests for InputCounts measurement algorithm. 

23""" 

24import numpy as np 

25import itertools 

26from collections import namedtuple 

27 

28import unittest 

29import lsst.utils.tests 

30 

31import lsst.geom 

32import lsst.afw.detection as afwDetection 

33import lsst.afw.geom as afwGeom 

34import lsst.afw.image as afwImage 

35import lsst.afw.table as afwTable 

36import lsst.meas.base as measBase 

37 

38 

39try: 

40 display 

41 import matplotlib.pyplot as plt 

42 import matplotlib.patches as patches 

43except NameError: 

44 display = False 

45 

46 

47def ccdVennDiagram(exp, showImage=True, legendLocation='best'): 

48 """Display and exposure & bounding boxes for images which go into a coadd. 

49 

50 Parameters 

51 ---------- 

52 exp : `lsst.afw.image.Exposure` 

53 The exposure object to plot, must be the product of a coadd 

54 showImage : `bool`, optional 

55 Plot image data in addition to its bounding box 

56 legendLocation : `str`, optional 

57 Matplotlib legend location code. Can be: ``'best'``, ``'upper 

58 right'``, ``'upper left'``, ``'lower left'``, ``'lower right'``, 

59 ``'right'``, ``'center left'``, ``'center right'``, ``'lower 

60 center'``, ``'upper center'``, ``'center'`` 

61 

62 """ 

63 # Create the figure object 

64 fig = plt.figure() 

65 # Use all the built in matplotib line style attributes to create a list of 

66 # the possible styles 

67 linestyles = ['solid', 'dashed', 'dashdot', 'dotted'] 

68 colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k'] 

69 # Calculate the cartisian product of the styles, and randomize the order, 

70 # to help each CCD get it's own color 

71 pcomb = np.random.permutation(list(itertools.product(colors, linestyles))) 

72 # Filter out a black solid box, as that will be the style of the given exp 

73 # object 

74 pcomb = pcomb[((pcomb[:, 0] == 'k') * (pcomb[:, 1] == 'solid')) is False] 

75 # Get the image properties 

76 origin = lsst.geom.PointD(exp.getXY0()) 

77 mainBox = exp.getBBox().getCorners() 

78 # Plot the exposure 

79 plt.gca().add_patch(patches.Rectangle((0, 0), *list(mainBox[2]-mainBox[0]), fill=False, label="exposure")) 

80 # Grab all of the CCDs that went into creating the exposure 

81 ccds = exp.getInfo().getCoaddInputs().ccds 

82 # Loop over and plot the extents of each ccd 

83 for i, ccd in enumerate(ccds): 

84 ccdBox = lsst.geom.Box2D(ccd.getBBox()) 

85 ccdCorners = ccdBox.getCorners() 

86 coaddCorners = [exp.getWcs().skyToPixel(ccd.getWcs().pixelToSky(point)) 

87 + (lsst.geom.PointD() - origin) for point in ccdCorners] 

88 plt.gca().add_patch(patches.Rectangle(coaddCorners[0], *list(coaddCorners[2]-coaddCorners[0]), 

89 fill=False, color=pcomb[i][0], ls=pcomb[i][1], 

90 label="CCD{}".format(i))) 

91 # If showImage is true, plot the data contained in exp as well as the 

92 # boundaries 

93 if showImage: 

94 plt.imshow(exp.image.array, cmap='Greys', origin='lower') 

95 plt.colorbar() 

96 # Adjust plot parameters and plot 

97 plt.gca().relim() 

98 plt.gca().autoscale_view() 

99 ylim = plt.gca().get_ylim() 

100 xlim = plt.gca().get_xlim() 

101 plt.gca().set_ylim(1.5*ylim[0], 1.5*ylim[1]) 

102 plt.gca().set_xlim(1.5*xlim[0], 1.5*xlim[1]) 

103 plt.legend(loc=legendLocation) 

104 fig.canvas.draw() 

105 plt.show() 

106 

107 

108class InputCountTest(lsst.utils.tests.TestCase): 

109 

110 def testInputCounts(self, showPlot=False): 

111 # Generate a simulated coadd of four overlapping-but-offset CCDs. 

112 # Populate it with three sources. 

113 # Demonstrate that we can correctly recover the number of images which 

114 # contribute to each source. 

115 

116 size = 20 # Size of images (pixels) 

117 value = 100.0 # Source flux 

118 

119 ccdPositions = [ 

120 lsst.geom.Point2D(8, 0), 

121 lsst.geom.Point2D(10, 10), 

122 lsst.geom.Point2D(-8, -8), 

123 lsst.geom.Point2D(-8, 8) 

124 ] 

125 

126 # Represent sources by a tuple of position and expected number of 

127 # contributing CCDs (based on the size/positions given above). 

128 Source = namedtuple("Source", ["pos", "count"]) 

129 sources = [ 

130 Source(pos=lsst.geom.Point2D(6, 6), count=2), 

131 Source(pos=lsst.geom.Point2D(10, 10), count=3), 

132 Source(pos=lsst.geom.Point2D(14, 14), count=1) 

133 ] 

134 

135 # These lines are used in the creation of WCS information 

136 scale = 1.0e-5 * lsst.geom.degrees 

137 cdMatrix = afwGeom.makeCdMatrix(scale=scale) 

138 crval = lsst.geom.SpherePoint(0.0, 0.0, lsst.geom.degrees) 

139 

140 # Construct the info needed to set the exposure object 

141 imageBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(size, size)) 

142 wcsRef = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0, 0), crval=crval, cdMatrix=cdMatrix) 

143 

144 # Create the exposure object, and set it up to be the output of a coadd 

145 exp = afwImage.ExposureF(size, size) 

146 exp.setWcs(wcsRef) 

147 exp.getInfo().setCoaddInputs(afwImage.CoaddInputs(afwTable.ExposureTable.makeMinimalSchema(), 

148 afwTable.ExposureTable.makeMinimalSchema())) 

149 

150 # Set the fake CCDs that "went into" making this coadd, using the 

151 # differing wcs objects created above. 

152 ccds = exp.getInfo().getCoaddInputs().ccds 

153 for pos in ccdPositions: 

154 record = ccds.addNew() 

155 record.setWcs(afwGeom.makeSkyWcs(crpix=pos, crval=crval, cdMatrix=cdMatrix)) 

156 record.setBBox(imageBox) 

157 record.setValidPolygon(afwGeom.Polygon(lsst.geom.Box2D(imageBox))) 

158 

159 # Configure a SingleFrameMeasurementTask to run InputCounts. 

160 measureSourcesConfig = measBase.SingleFrameMeasurementConfig() 

161 measureSourcesConfig.plugins.names = ["base_PeakCentroid", "base_InputCount"] 

162 measureSourcesConfig.slots.centroid = "base_PeakCentroid" 

163 measureSourcesConfig.slots.psfFlux = None 

164 measureSourcesConfig.slots.apFlux = None 

165 measureSourcesConfig.slots.modelFlux = None 

166 measureSourcesConfig.slots.gaussianFlux = None 

167 measureSourcesConfig.slots.calibFlux = None 

168 measureSourcesConfig.slots.shape = None 

169 measureSourcesConfig.validate() 

170 schema = afwTable.SourceTable.makeMinimalSchema() 

171 task = measBase.SingleFrameMeasurementTask(schema, config=measureSourcesConfig) 

172 catalog = afwTable.SourceCatalog(schema) 

173 

174 # Add simulated sources to the measurement catalog. 

175 for src in sources: 

176 spans = afwGeom.SpanSet.fromShape(1) 

177 spans = spans.shiftedBy(int(src.pos.getX()), int(src.pos.getY())) 

178 foot = afwDetection.Footprint(spans) 

179 peak = foot.getPeaks().addNew() 

180 peak.setFx(src.pos[0]) 

181 peak.setFy(src.pos[1]) 

182 peak.setPeakValue(value) 

183 catalog.addNew().setFootprint(foot) 

184 

185 task.run(catalog, exp) 

186 

187 for src, rec in zip(sources, catalog): 

188 self.assertEqual(rec.get("base_InputCount_value"), src.count) 

189 

190 if display: 

191 ccdVennDiagram(exp) 

192 

193 def _preparePlugin(self, addCoaddInputs): 

194 """Prepare a `SingleFrameInputCountPlugin` for running. 

195 

196 Sets up the plugin to run on an empty catalog together with a 

197 synthetic, content-free `~lsst.afw.image.ExposureF`. 

198 

199 Parameters 

200 ---------- 

201 addCoaddInputs : `bool` 

202 Should we add the coadd inputs? 

203 

204 Returns 

205 ------- 

206 inputCount : `SingleFrameInputCountPlugin` 

207 Initialized measurement plugin. 

208 catalog : `lsst.afw.table.SourceCatalog` 

209 Empty Catalog. 

210 exp : `lsst.afw.image.ExposureF` 

211 Synthetic exposure. 

212 """ 

213 exp = afwImage.ExposureF(20, 20) 

214 scale = 1.0e-5*lsst.geom.degrees 

215 wcs = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0, 0), 

216 crval=lsst.geom.SpherePoint(0.0, 0.0, lsst.geom.degrees), 

217 cdMatrix=afwGeom.makeCdMatrix(scale=scale)) 

218 exp.setWcs(wcs) 

219 if addCoaddInputs: 

220 exp.getInfo().setCoaddInputs(afwImage.CoaddInputs(afwTable.ExposureTable.makeMinimalSchema(), 

221 afwTable.ExposureTable.makeMinimalSchema())) 

222 ccds = exp.getInfo().getCoaddInputs().ccds 

223 record = ccds.addNew() 

224 record.setWcs(wcs) 

225 record.setBBox(exp.getBBox()) 

226 record.setValidPolygon(afwGeom.Polygon(lsst.geom.Box2D(exp.getBBox()))) 

227 

228 schema = afwTable.SourceTable.makeMinimalSchema() 

229 measBase.SingleFramePeakCentroidPlugin(measBase.SingleFramePeakCentroidConfig(), 

230 "centroid", schema, None) 

231 schema.getAliasMap().set("slot_Centroid", "centroid") 

232 inputCount = measBase.SingleFrameInputCountPlugin(measBase.InputCountConfig(), 

233 "inputCount", schema, None) 

234 catalog = afwTable.SourceCatalog(schema) 

235 return inputCount, catalog, exp 

236 

237 def testBadCentroid(self): 

238 """Test that a NaN centroid raises and results in a correct flag. 

239 

240 The flag from the centroid slot should propagate to the badCentroid 

241 flag on InputCount and the algorithm should throw a MeasurementError 

242 when it encounters a NaN position. 

243 """ 

244 inputCount, catalog, exp = self._preparePlugin(True) 

245 record = catalog.addNew() 

246 

247 # The inputCount's badCentroid flag is an alias to the centroid's 

248 # global flag, so it should be set immediately. 

249 record.set("centroid_flag", True) 

250 self.assertTrue(record.get("inputCount_flag_badCentroid")) 

251 

252 # Even though the source is flagged as bad, if the position is good we 

253 # should still get a measurement. 

254 record.set("slot_Centroid_x", 10) 

255 record.set("slot_Centroid_y", 10) 

256 inputCount.measure(record, exp) 

257 self.assertTrue(record.get("inputCount_flag_badCentroid")) 

258 self.assertEqual(record.get("inputCount_value"), 1) 

259 

260 # The centroid is bad (with a NaN) even though the centroid isn't 

261 # flagged, so we should get a MeasurementError indicating an expected 

262 # failure. 

263 record = catalog.addNew() 

264 record.set("slot_Centroid_x", float("nan")) 

265 record.set("slot_Centroid_y", 12.345) 

266 record.set("centroid_flag", False) 

267 with self.assertRaises(measBase.MeasurementError) as measErr: 

268 inputCount.measure(record, exp) 

269 

270 # Calling the fail() method should set the global flag. 

271 record = catalog.addNew() 

272 record.set("inputCount_flag", False) 

273 inputCount.fail(record, measErr.exception) 

274 self.assertTrue(record.get("inputCount_flag")) 

275 

276 def testBadCoaddInputs(self): 

277 """Test that no coadd inputs raises and results in a correct flag. 

278 

279 When there are no coadd inputs on the input exposure we should throw a 

280 MeasurementError and set both the global flag and flag_noInputs. 

281 """ 

282 inputCount, catalog, exp = self._preparePlugin(False) 

283 record = catalog.addNew() 

284 

285 # Initially, the record is not flagged. 

286 self.assertFalse(record.get("inputCount_flag")) 

287 self.assertFalse(record.get("inputCount_flag_noInputs")) 

288 

289 # There are no coadd inputs, so we should get a MeasurementError 

290 # indicating an expected failure. 

291 with self.assertRaises(measBase.MeasurementError) as measErr: 

292 inputCount.measure(record, exp) 

293 

294 # Calling the fail() method should set the noInputs and global flags. 

295 inputCount.fail(record, measErr.exception) 

296 self.assertTrue(record.get("inputCount_flag")) 

297 self.assertTrue(record.get("inputCount_flag_noInputs")) 

298 

299 

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

301 pass 

302 

303 

304def setup_module(module): 

305 lsst.utils.tests.init() 

306 

307 

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

309 lsst.utils.tests.init() 

310 unittest.main()