Coverage for tests/test_InputCount.py: 16%
148 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-12 02:16 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-12 02:16 -0800
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/>.
22"""Tests for InputCounts measurement algorithm.
23"""
24import numpy as np
25import itertools
26from collections import namedtuple
28import unittest
29import lsst.utils.tests
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
39try:
40 display
41 import matplotlib.pyplot as plt
42 import matplotlib.patches as patches
43except NameError:
44 display = False
47def ccdVennDiagram(exp, showImage=True, legendLocation='best'):
48 """Display and exposure & bounding boxes for images which go into a coadd.
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'``
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.getMaskedImage().getArrays()[0], 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()
108class InputCountTest(lsst.utils.tests.TestCase):
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.
116 size = 20 # Size of images (pixels)
117 value = 100.0 # Source flux
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 ]
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 ]
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)
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)
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()))
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)))
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)
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)
185 task.run(catalog, exp)
187 for src, rec in zip(sources, catalog):
188 self.assertEqual(rec.get("base_InputCount_value"), src.count)
190 if display:
191 ccdVennDiagram(exp)
193 def _preparePlugin(self, addCoaddInputs):
194 """Prepare a `SingleFrameInputCountPlugin` for running.
196 Sets up the plugin to run on an empty catalog together with a
197 synthetic, content-free `~lsst.afw.image.ExposureF`.
199 Parameters
200 ----------
201 addCoaddInputs : `bool`
202 Should we add the coadd inputs?
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())))
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
237 def testBadCentroid(self):
238 """Test that a NaN centroid raises and results in a correct flag.
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()
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"))
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)
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)
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"))
276 def testBadCoaddInputs(self):
277 """Test that no coadd inputs raises and results in a correct flag.
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()
285 # Initially, the record is not flagged.
286 self.assertFalse(record.get("inputCount_flag"))
287 self.assertFalse(record.get("inputCount_flag_noInputs"))
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)
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"))
300class TestMemory(lsst.utils.tests.MemoryTestCase):
301 pass
304def setup_module(module):
305 lsst.utils.tests.init()
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()