Coverage for tests/test_astrometryTask.py: 16%
184 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-26 11:05 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-26 11:05 +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 afwTable.CoordKey.addErrorFields(sourceSchema)
87 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
88 # schema must be passed to the solver task constructor
89 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema)
90 sourceCat = self.makeSourceCat(self.tanWcs, sourceSchema=sourceSchema)
92 results = solver.run(
93 sourceCat=sourceCat,
94 exposure=self.exposure,
95 )
96 # check that the used flag is set the right number of times
97 count = 0
98 for source in sourceCat:
99 if source.get('calib_astrometry_used'):
100 count += 1
101 self.assertEqual(count, len(results.matches))
103 def testWcsFailure(self):
104 """In the case of a failed WCS fit, test that the exposure's WCS is set
105 to None and the coord_ra & coord_dec columns are set to nan in the
106 source catalog.
107 """
108 self.exposure.setWcs(self.tanWcs)
109 config = AstrometryTask.ConfigClass()
110 config.wcsFitter.order = 2
111 config.wcsFitter.maxScatterArcsec = 0.0 # To ensure a WCS failure
112 sourceSchema = afwTable.SourceTable.makeMinimalSchema()
113 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
114 # schema must be passed to the solver task constructor
115 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema)
116 sourceCat = self.makeSourceCat(self.tanWcs, sourceSchema=sourceSchema, doScatterCentroids=True)
117 with self.assertLogs(level=logging.WARNING) as cm:
118 results = solver.run(
119 sourceCat=sourceCat,
120 exposure=self.exposure,
121 )
122 logOutput = ";".join(cm.output)
123 self.assertIn("WCS fit failed.", logOutput)
124 self.assertIn("Setting exposure's WCS to None and coord_ra & coord_dec cols in sourceCat to nan.",
125 logOutput)
126 # Check that matches is set to None, the sourceCat coord cols are all
127 # set to nan and that the WCS attached to the exposure is set to None.
128 self.assertTrue(results.matches is None)
129 self.assertTrue(np.all(np.isnan(sourceCat["coord_ra"])))
130 self.assertTrue(np.all(np.isnan(sourceCat["coord_dec"])))
131 self.assertTrue(self.exposure.getWcs() is None)
132 self.assertTrue(results.scatterOnSky is None)
133 self.assertTrue(results.matches is None)
135 def doTest(self, pixelsToTanPixels, order=3):
136 """Test using pixelsToTanPixels to distort the source positions
137 """
138 distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels, wcs=self.tanWcs,
139 modifyActualPixels=False)
140 self.exposure.setWcs(distortedWcs)
141 sourceCat = self.makeSourceCat(distortedWcs)
142 config = AstrometryTask.ConfigClass()
143 config.wcsFitter.order = order
144 config.wcsFitter.numRejIter = 0
145 # This test is from before rough magnitude rejection was implemented.
146 config.doMagnitudeOutlierRejection = False
147 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
148 results = solver.run(
149 sourceCat=sourceCat,
150 exposure=self.exposure,
151 )
152 fitWcs = self.exposure.getWcs()
153 self.assertRaises(Exception, self.assertWcsAlmostEqualOverBBox, fitWcs, distortedWcs)
154 self.assertWcsAlmostEqualOverBBox(distortedWcs, fitWcs, self.bbox,
155 maxDiffSky=0.01*lsst.geom.arcseconds, maxDiffPix=0.02)
157 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"])
158 refCoordKey = afwTable.CoordKey(results.refCat.schema["coord"])
159 refCentroidKey = afwTable.Point2DKey(results.refCat.schema["centroid"])
160 maxAngSep = 0*lsst.geom.radians
161 maxPixSep = 0
162 for refObj, src, d in results.matches:
163 refCoord = refObj.get(refCoordKey)
164 refPixPos = refObj.get(refCentroidKey)
165 srcCoord = src.get(srcCoordKey)
166 srcPixPos = src.getCentroid()
168 angSep = refCoord.separation(srcCoord)
169 maxAngSep = max(maxAngSep, angSep)
171 pixSep = math.hypot(*(srcPixPos-refPixPos))
172 maxPixSep = max(maxPixSep, pixSep)
173 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),))
174 print("max pixel separation = %0.3f" % (maxPixSep,))
175 self.assertLess(maxAngSep.asArcseconds(), 0.0038)
176 self.assertLess(maxPixSep, 0.021)
178 # try again, invoking the reference selector
179 config.referenceSelector.doUnresolved = True
180 config.referenceSelector.unresolved.name = 'resolved'
181 solverRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
182 self.exposure.setWcs(distortedWcs)
183 resultsRefSelect = solverRefSelect.run(
184 sourceCat=sourceCat,
185 exposure=self.exposure,
186 )
187 self.assertLess(len(resultsRefSelect.matches), len(results.matches))
189 # try again, allowing magnitude outlier rejection.
190 config.doMagnitudeOutlierRejection = True
191 solverMagOutlierRejection = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
192 self.exposure.setWcs(distortedWcs)
193 resultsMagOutlierRejection = solverMagOutlierRejection.run(
194 sourceCat=sourceCat,
195 exposure=self.exposure,
196 )
197 self.assertLess(len(resultsMagOutlierRejection.matches), len(resultsRefSelect.matches))
198 config.doMagnitudeOutlierRejection = False
200 # try again, but without fitting the WCS, no reference selector
201 config.referenceSelector.doUnresolved = False
202 config.forceKnownWcs = True
203 solverNoFit = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
204 self.exposure.setWcs(distortedWcs)
205 resultsNoFit = solverNoFit.run(
206 sourceCat=sourceCat,
207 exposure=self.exposure,
208 )
209 self.assertIsNone(resultsNoFit.scatterOnSky)
211 # fitting should result in matches that are at least as good
212 # (strictly speaking fitting might result in a larger match list with
213 # some outliers, but in practice this test passes)
214 meanFitDist = np.mean([match.distance for match in results.matches])
215 meanNoFitDist = np.mean([match.distance for match in resultsNoFit.matches])
216 self.assertLessEqual(meanFitDist, meanNoFitDist)
218 # try once again, without fitting the WCS, with the reference selector
219 # (this goes through a different code path)
220 config.referenceSelector.doUnresolved = True
221 solverNoFitRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader)
222 resultsNoFitRefSelect = solverNoFitRefSelect.run(
223 sourceCat=sourceCat,
224 exposure=self.exposure,
225 )
226 self.assertLess(len(resultsNoFitRefSelect.matches), len(resultsNoFit.matches))
228 def makeSourceCat(self, wcs, sourceSchema=None, doScatterCentroids=False):
229 """Make a source catalog by reading the position reference stars using
230 the proviced WCS.
232 Optionally provide a schema for the source catalog (to allow
233 AstrometryTask in the test methods to update it with the
234 "calib_astrometry_used" flag). Otherwise, a minimal SourceTable
235 schema will be created.
237 Optionally, via doScatterCentroids, add some scatter to the centroids
238 assiged to the source catalog (otherwise they will be identical to
239 those of the reference catalog).
240 """
241 loadRes = self.refObjLoader.loadPixelBox(bbox=self.bbox, wcs=wcs, filterName="r")
242 refCat = loadRes.refCat
244 if sourceSchema is None:
245 sourceSchema = afwTable.SourceTable.makeMinimalSchema()
246 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema
247 afwTable.CoordKey.addErrorFields(sourceSchema)
248 sourceCat = afwTable.SourceCatalog(sourceSchema)
250 sourceCat.resize(len(refCat))
251 scatterFactor = 1.0
252 if doScatterCentroids:
253 np.random.seed(12345)
254 scatterFactor = np.random.uniform(0.999, 1.001, len(sourceCat))
255 sourceCat["slot_Centroid_x"] = scatterFactor*refCat["centroid_x"]
256 sourceCat["slot_Centroid_y"] = scatterFactor*refCat["centroid_y"]
257 sourceCat["slot_ApFlux_instFlux"] = refCat["r_flux"]
258 sourceCat["slot_ApFlux_instFluxErr"] = refCat["r_flux"]/100
260 # Deliberately add some outliers to check that the magnitude
261 # outlier rejection code is being run.
262 sourceCat["slot_ApFlux_instFlux"][0: 4] *= 1000.0
264 return sourceCat
267class TestMagnitudeOutliers(lsst.utils.tests.TestCase):
268 def testMagnitudeOutlierRejection(self):
269 """Test rejection of magnitude outliers.
271 This test only tests the outlier rejection, and not any other
272 part of the matching or astrometry fitter.
273 """
274 config = AstrometryTask.ConfigClass()
275 config.doMagnitudeOutlierRejection = True
276 config.magnitudeOutlierRejectionNSigma = 4.0
277 solver = AstrometryTask(config=config, refObjLoader=None)
279 nTest = 100
281 refSchema = lsst.afw.table.SimpleTable.makeMinimalSchema()
282 refSchema.addField('refFlux', 'F')
283 refCat = lsst.afw.table.SimpleCatalog(refSchema)
284 refCat.resize(nTest)
286 srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
287 srcSchema.addField('srcFlux', 'F')
288 srcCat = lsst.afw.table.SourceCatalog(srcSchema)
289 srcCat.resize(nTest)
291 np.random.seed(12345)
292 refMag = np.full(nTest, 20.0)
293 srcMag = np.random.normal(size=nTest, loc=0.0, scale=1.0)
295 # Determine the sigma of the random sample
296 zp = np.median(refMag[: -4] - srcMag[: -4])
297 sigma = scipy.stats.median_abs_deviation(srcMag[: -4], scale='normal')
299 # Deliberately alter some magnitudes to be outliers.
300 srcMag[-3] = (config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp)
301 srcMag[-4] = -(config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp)
303 refCat['refFlux'] = (refMag*units.ABmag).to_value(units.nJy)
304 srcCat['srcFlux'] = 10.0**(srcMag/(-2.5))
306 # Deliberately poison some reference fluxes.
307 refCat['refFlux'][-1] = np.inf
308 refCat['refFlux'][-2] = np.nan
310 matchesIn = []
311 for ref, src in zip(refCat, srcCat):
312 matchesIn.append(lsst.afw.table.ReferenceMatch(first=ref, second=src, distance=0.0))
314 matchesOut = solver._removeMagnitudeOutliers('srcFlux', 'refFlux', matchesIn)
316 # We should lose the 4 outliers we created.
317 self.assertEqual(len(matchesOut), len(matchesIn) - 4)
320class MemoryTester(lsst.utils.tests.MemoryTestCase):
321 pass
324def setup_module(module):
325 lsst.utils.tests.init()
328if __name__ == "__main__": 328 ↛ 329line 328 didn't jump to line 329, because the condition on line 328 was never true
329 lsst.utils.tests.init()
330 unittest.main()