Coverage for tests/test_astrometryTask.py: 16%
181 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 09:57 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 09:57 +0000
1#
2# LSST Data Management System
3# Copyright 2008-2017 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
23import logging
24import os.path
25import math
26import unittest
27import glob
29from astropy import units
30import scipy.stats
31import numpy as np
33import lsst.utils.tests
34import lsst.geom
35import lsst.afw.geom as afwGeom
36import lsst.afw.table as afwTable
37import lsst.afw.image as afwImage
38import lsst.meas.base as measBase
39from lsst.meas.algorithms.testUtils import MockReferenceObjectLoaderFromFiles
40from lsst.meas.astrom import AstrometryTask
43class TestAstrometricSolver(lsst.utils.tests.TestCase):
45 def setUp(self):
46 refCatDir = os.path.join(os.path.dirname(__file__), "data", "sdssrefcat")
48 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(3001, 3001))
49 crpix = lsst.geom.Box2D(self.bbox).getCenter()
50 self.tanWcs = afwGeom.makeSkyWcs(crpix=crpix,
51 crval=lsst.geom.SpherePoint(215.5, 53.0, lsst.geom.degrees),
52 cdMatrix=afwGeom.makeCdMatrix(scale=5.1e-5*lsst.geom.degrees))
53 self.exposure = afwImage.ExposureF(self.bbox)
54 self.exposure.setWcs(self.tanWcs)
55 self.exposure.setFilter(afwImage.FilterLabel(band="r", physical="rTest"))
56 filenames = sorted(glob.glob(os.path.join(refCatDir, 'ref_cats', 'cal_ref_cat', '??????.fits')))
57 self.refObjLoader = MockReferenceObjectLoaderFromFiles(filenames, htmLevel=8)
59 def tearDown(self):
60 del self.tanWcs
61 del self.exposure
62 del self.refObjLoader
64 def testTrivial(self):
65 """Test fit with no distortion
66 """
67 self.doTest(afwGeom.makeIdentityTransform())
69 def testRadial(self):
70 """Test fit with radial distortion
72 The offset comes from the fact that the CCD is not centered
73 """
74 self.doTest(afwGeom.makeRadialTransform([0, 1.01, 1e-7]))
76 def testUsedFlag(self):
77 """Test that the solver will record number of sources used to table
78 if it is passed a schema on initialization.
79 """
80 self.exposure.setWcs(self.tanWcs)
81 config = AstrometryTask.ConfigClass()
82 config.wcsFitter.order = 2
83 config.wcsFitter.numRejIter = 0
85 sourceSchema = afwTable.SourceTable.makeMinimalSchema()
86 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
87 # schema must be passed to the solver task constructor
88 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema)
89 sourceCat = self.makeSourceCat(self.tanWcs, sourceSchema=sourceSchema)
91 results = solver.run(
92 sourceCat=sourceCat,
93 exposure=self.exposure,
94 )
95 # check that the used flag is set the right number of times
96 count = 0
97 for source in sourceCat:
98 if source.get('calib_astrometry_used'):
99 count += 1
100 self.assertEqual(count, len(results.matches))
102 def testWcsFailure(self):
103 """In the case of a failed WCS fit, test that the exposure's WCS is set
104 to None and the coord_ra & coord_dec columns are set to nan in the
105 source catalog.
106 """
107 self.exposure.setWcs(self.tanWcs)
108 config = AstrometryTask.ConfigClass()
109 config.wcsFitter.order = 2
110 config.wcsFitter.maxScatterArcsec = 0.0 # To ensure a WCS failure
111 sourceSchema = afwTable.SourceTable.makeMinimalSchema()
112 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
113 # schema must be passed to the solver task constructor
114 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema)
115 sourceCat = self.makeSourceCat(self.tanWcs, sourceSchema=sourceSchema, doScatterCentroids=True)
116 with self.assertLogs(level=logging.WARNING) as cm:
117 results = solver.run(
118 sourceCat=sourceCat,
119 exposure=self.exposure,
120 )
121 logOutput = ";".join(cm.output)
122 self.assertIn("WCS fit failed.", logOutput)
123 self.assertIn("Setting exposure's WCS to None and coord_ra & coord_dec cols in sourceCat to nan.",
124 logOutput)
125 # Check that matches is set to None, the sourceCat coord cols are all
126 # set to nan and that the WCS attached to the exposure is set to None.
127 self.assertTrue(results.matches is None)
128 self.assertTrue(np.all(np.isnan(sourceCat["coord_ra"])))
129 self.assertTrue(np.all(np.isnan(sourceCat["coord_dec"])))
130 self.assertTrue(self.exposure.getWcs() is None)
131 self.assertTrue(results.scatterOnSky is None)
132 self.assertTrue(results.matches is None)
134 def doTest(self, pixelsToTanPixels, order=3):
135 """Test using pixelsToTanPixels to distort the source positions
136 """
137 distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels, wcs=self.tanWcs,
138 modifyActualPixels=False)
139 self.exposure.setWcs(distortedWcs)
140 sourceCat = self.makeSourceCat(distortedWcs)
141 config = AstrometryTask.ConfigClass()
142 config.wcsFitter.order = order
143 config.wcsFitter.numRejIter = 0
144 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
145 results = solver.run(
146 sourceCat=sourceCat,
147 exposure=self.exposure,
148 )
149 fitWcs = self.exposure.getWcs()
150 self.assertRaises(Exception, self.assertWcsAlmostEqualOverBBox, fitWcs, distortedWcs)
151 self.assertWcsAlmostEqualOverBBox(distortedWcs, fitWcs, self.bbox,
152 maxDiffSky=0.01*lsst.geom.arcseconds, maxDiffPix=0.02)
154 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"])
155 refCoordKey = afwTable.CoordKey(results.refCat.schema["coord"])
156 refCentroidKey = afwTable.Point2DKey(results.refCat.schema["centroid"])
157 maxAngSep = 0*lsst.geom.radians
158 maxPixSep = 0
159 for refObj, src, d in results.matches:
160 refCoord = refObj.get(refCoordKey)
161 refPixPos = refObj.get(refCentroidKey)
162 srcCoord = src.get(srcCoordKey)
163 srcPixPos = src.getCentroid()
165 angSep = refCoord.separation(srcCoord)
166 maxAngSep = max(maxAngSep, angSep)
168 pixSep = math.hypot(*(srcPixPos-refPixPos))
169 maxPixSep = max(maxPixSep, pixSep)
170 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),))
171 print("max pixel separation = %0.3f" % (maxPixSep,))
172 self.assertLess(maxAngSep.asArcseconds(), 0.0038)
173 self.assertLess(maxPixSep, 0.021)
175 # try again, invoking the reference selector
176 config.referenceSelector.doUnresolved = True
177 config.referenceSelector.unresolved.name = 'resolved'
178 solverRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
179 self.exposure.setWcs(distortedWcs)
180 resultsRefSelect = solverRefSelect.run(
181 sourceCat=sourceCat,
182 exposure=self.exposure,
183 )
184 self.assertLess(len(resultsRefSelect.matches), len(results.matches))
186 # try again, allowing magnitude outlier rejection.
187 config.doMagnitudeOutlierRejection = True
188 solverMagOutlierRejection = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
189 self.exposure.setWcs(distortedWcs)
190 resultsMagOutlierRejection = solverMagOutlierRejection.run(
191 sourceCat=sourceCat,
192 exposure=self.exposure,
193 )
194 self.assertLess(len(resultsMagOutlierRejection.matches), len(resultsRefSelect.matches))
195 config.doMagnitudeOutlierRejection = False
197 # try again, but without fitting the WCS, no reference selector
198 config.referenceSelector.doUnresolved = False
199 config.forceKnownWcs = True
200 solverNoFit = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
201 self.exposure.setWcs(distortedWcs)
202 resultsNoFit = solverNoFit.run(
203 sourceCat=sourceCat,
204 exposure=self.exposure,
205 )
206 self.assertIsNone(resultsNoFit.scatterOnSky)
208 # fitting should result in matches that are at least as good
209 # (strictly speaking fitting might result in a larger match list with
210 # some outliers, but in practice this test passes)
211 meanFitDist = np.mean([match.distance for match in results.matches])
212 meanNoFitDist = np.mean([match.distance for match in resultsNoFit.matches])
213 self.assertLessEqual(meanFitDist, meanNoFitDist)
215 # try once again, without fitting the WCS, with the reference selector
216 # (this goes through a different code path)
217 config.referenceSelector.doUnresolved = True
218 solverNoFitRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
219 resultsNoFitRefSelect = solverNoFitRefSelect.run(
220 sourceCat=sourceCat,
221 exposure=self.exposure,
222 )
223 self.assertLess(len(resultsNoFitRefSelect.matches), len(resultsNoFit.matches))
225 def makeSourceCat(self, wcs, sourceSchema=None, doScatterCentroids=False):
226 """Make a source catalog by reading the position reference stars using
227 the proviced WCS.
229 Optionally provide a schema for the source catalog (to allow
230 AstrometryTask in the test methods to update it with the
231 "calib_astrometry_used" flag). Otherwise, a minimal SourceTable
232 schema will be created.
234 Optionally, via doScatterCentroids, add some scatter to the centroids
235 assiged to the source catalog (otherwise they will be identical to
236 those of the reference catalog).
237 """
238 loadRes = self.refObjLoader.loadPixelBox(bbox=self.bbox, wcs=wcs, filterName="r")
239 refCat = loadRes.refCat
241 if sourceSchema is None:
242 sourceSchema = afwTable.SourceTable.makeMinimalSchema()
243 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
244 sourceCat = afwTable.SourceCatalog(sourceSchema)
246 sourceCat.resize(len(refCat))
247 scatterFactor = 1.0
248 if doScatterCentroids:
249 np.random.seed(12345)
250 scatterFactor = np.random.uniform(0.999, 1.001, len(sourceCat))
251 sourceCat["slot_Centroid_x"] = scatterFactor*refCat["centroid_x"]
252 sourceCat["slot_Centroid_y"] = scatterFactor*refCat["centroid_y"]
253 sourceCat["slot_ApFlux_instFlux"] = refCat["r_flux"]
254 sourceCat["slot_ApFlux_instFluxErr"] = refCat["r_flux"]/100
256 # Deliberately add some outliers to check that the magnitude
257 # outlier rejection code is being run.
258 sourceCat["slot_ApFlux_instFlux"][0: 4] *= 1000.0
260 return sourceCat
263class TestMagnitudeOutliers(lsst.utils.tests.TestCase):
264 def testMagnitudeOutlierRejection(self):
265 """Test rejection of magnitude outliers.
267 This test only tests the outlier rejection, and not any other
268 part of the matching or astrometry fitter.
269 """
270 config = AstrometryTask.ConfigClass()
271 config.doMagnitudeOutlierRejection = True
272 config.magnitudeOutlierRejectionNSigma = 4.0
273 solver = AstrometryTask(config=config, refObjLoader=None)
275 nTest = 100
277 refSchema = lsst.afw.table.SimpleTable.makeMinimalSchema()
278 refSchema.addField('refFlux', 'F')
279 refCat = lsst.afw.table.SimpleCatalog(refSchema)
280 refCat.resize(nTest)
282 srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
283 srcSchema.addField('srcFlux', 'F')
284 srcCat = lsst.afw.table.SourceCatalog(srcSchema)
285 srcCat.resize(nTest)
287 np.random.seed(12345)
288 refMag = np.full(nTest, 20.0)
289 srcMag = np.random.normal(size=nTest, loc=0.0, scale=1.0)
291 # Determine the sigma of the random sample
292 zp = np.median(refMag[: -4] - srcMag[: -4])
293 sigma = scipy.stats.median_abs_deviation(srcMag[: -4], scale='normal')
295 # Deliberately alter some magnitudes to be outliers.
296 srcMag[-3] = (config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp)
297 srcMag[-4] = -(config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp)
299 refCat['refFlux'] = (refMag*units.ABmag).to_value(units.nJy)
300 srcCat['srcFlux'] = 10.0**(srcMag/(-2.5))
302 # Deliberately poison some reference fluxes.
303 refCat['refFlux'][-1] = np.inf
304 refCat['refFlux'][-2] = np.nan
306 matchesIn = []
307 for ref, src in zip(refCat, srcCat):
308 matchesIn.append(lsst.afw.table.ReferenceMatch(first=ref, second=src, distance=0.0))
310 matchesOut = solver._removeMagnitudeOutliers('srcFlux', 'refFlux', matchesIn)
312 # We should lose the 4 outliers we created.
313 self.assertEqual(len(matchesOut), len(matchesIn) - 4)
316class MemoryTester(lsst.utils.tests.MemoryTestCase):
317 pass
320def setup_module(module):
321 lsst.utils.tests.init()
324if __name__ == "__main__": 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 lsst.utils.tests.init()
326 unittest.main()