Coverage for tests/test_astrometryTask.py: 20%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

169 statements  

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# 

22 

23import os.path 

24import math 

25import unittest 

26import glob 

27 

28from astropy import units 

29import scipy.stats 

30import numpy as np 

31 

32import lsst.utils.tests 

33import lsst.geom 

34import lsst.afw.geom as afwGeom 

35import lsst.afw.table as afwTable 

36import lsst.afw.image as afwImage 

37import lsst.meas.base as measBase 

38import lsst.pipe.base as pipeBase 

39from lsst.meas.algorithms.testUtils import MockReferenceObjectLoaderFromFiles 

40from lsst.meas.astrom import AstrometryTask 

41 

42 

43class TestAstrometricSolver(lsst.utils.tests.TestCase): 

44 

45 def setUp(self): 

46 refCatDir = os.path.join(os.path.dirname(__file__), "data", "sdssrefcat") 

47 

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.setFilterLabel(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) 

58 

59 def tearDown(self): 

60 del self.tanWcs 

61 del self.exposure 

62 del self.refObjLoader 

63 

64 def testTrivial(self): 

65 """Test fit with no distortion 

66 """ 

67 self.doTest(afwGeom.makeIdentityTransform()) 

68 

69 def testRadial(self): 

70 """Test fit with radial distortion 

71 

72 The offset comes from the fact that the CCD is not centered 

73 """ 

74 self.doTest(afwGeom.makeRadialTransform([0, 1.01, 1e-7])) 

75 

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 

84 

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) 

90 

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)) 

101 

102 def testMaxMeanDistance(self): 

103 """If the astrometric fit does not satisfy the maxMeanDistanceArcsec 

104 threshold, ensure task raises an lsst.pipe.base.TaskError. 

105 """ 

106 self.exposure.setWcs(self.tanWcs) 

107 config = AstrometryTask.ConfigClass() 

108 config.maxMeanDistanceArcsec = 0.0 # To ensure a "deemed" WCS failure 

109 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

110 sourceCat = self.makeSourceCat(self.tanWcs, doScatterCentroids=True) 

111 

112 with self.assertRaisesRegex(pipeBase.TaskError, "Fatal astrometry failure detected"): 

113 solver.run(sourceCat=sourceCat, exposure=self.exposure) 

114 

115 def doTest(self, pixelsToTanPixels, order=3): 

116 """Test using pixelsToTanPixels to distort the source positions 

117 """ 

118 distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels, wcs=self.tanWcs, 

119 modifyActualPixels=False) 

120 self.exposure.setWcs(distortedWcs) 

121 sourceCat = self.makeSourceCat(distortedWcs) 

122 config = AstrometryTask.ConfigClass() 

123 config.wcsFitter.order = order 

124 config.wcsFitter.numRejIter = 0 

125 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

126 results = solver.run( 

127 sourceCat=sourceCat, 

128 exposure=self.exposure, 

129 ) 

130 fitWcs = self.exposure.getWcs() 

131 self.assertRaises(Exception, self.assertWcsAlmostEqualOverBBox, fitWcs, distortedWcs) 

132 self.assertWcsAlmostEqualOverBBox(distortedWcs, fitWcs, self.bbox, 

133 maxDiffSky=0.01*lsst.geom.arcseconds, maxDiffPix=0.02) 

134 

135 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"]) 

136 refCoordKey = afwTable.CoordKey(results.refCat.schema["coord"]) 

137 refCentroidKey = afwTable.Point2DKey(results.refCat.schema["centroid"]) 

138 maxAngSep = 0*lsst.geom.radians 

139 maxPixSep = 0 

140 for refObj, src, d in results.matches: 

141 refCoord = refObj.get(refCoordKey) 

142 refPixPos = refObj.get(refCentroidKey) 

143 srcCoord = src.get(srcCoordKey) 

144 srcPixPos = src.getCentroid() 

145 

146 angSep = refCoord.separation(srcCoord) 

147 maxAngSep = max(maxAngSep, angSep) 

148 

149 pixSep = math.hypot(*(srcPixPos-refPixPos)) 

150 maxPixSep = max(maxPixSep, pixSep) 

151 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),)) 

152 print("max pixel separation = %0.3f" % (maxPixSep,)) 

153 self.assertLess(maxAngSep.asArcseconds(), 0.0038) 

154 self.assertLess(maxPixSep, 0.021) 

155 

156 # try again, invoking the reference selector 

157 config.referenceSelector.doUnresolved = True 

158 config.referenceSelector.unresolved.name = 'resolved' 

159 solverRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

160 self.exposure.setWcs(distortedWcs) 

161 resultsRefSelect = solverRefSelect.run( 

162 sourceCat=sourceCat, 

163 exposure=self.exposure, 

164 ) 

165 self.assertLess(len(resultsRefSelect.matches), len(results.matches)) 

166 

167 # try again, allowing magnitude outlier rejection. 

168 config.doMagnitudeOutlierRejection = True 

169 solverMagOutlierRejection = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

170 self.exposure.setWcs(distortedWcs) 

171 resultsMagOutlierRejection = solverMagOutlierRejection.run( 

172 sourceCat=sourceCat, 

173 exposure=self.exposure, 

174 ) 

175 self.assertLess(len(resultsMagOutlierRejection.matches), len(resultsRefSelect.matches)) 

176 config.doMagnitudeOutlierRejection = False 

177 

178 # try again, but without fitting the WCS, no reference selector 

179 config.referenceSelector.doUnresolved = False 

180 config.forceKnownWcs = True 

181 solverNoFit = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

182 self.exposure.setWcs(distortedWcs) 

183 resultsNoFit = solverNoFit.run( 

184 sourceCat=sourceCat, 

185 exposure=self.exposure, 

186 ) 

187 self.assertIsNone(resultsNoFit.scatterOnSky) 

188 

189 # fitting should result in matches that are at least as good 

190 # (strictly speaking fitting might result in a larger match list with 

191 # some outliers, but in practice this test passes) 

192 meanFitDist = np.mean([match.distance for match in results.matches]) 

193 meanNoFitDist = np.mean([match.distance for match in resultsNoFit.matches]) 

194 self.assertLessEqual(meanFitDist, meanNoFitDist) 

195 

196 # try once again, without fitting the WCS, with the reference selector 

197 # (this goes through a different code path) 

198 config.referenceSelector.doUnresolved = True 

199 solverNoFitRefSelect = AstrometryTask(config=config, refObjLoader=self.refObjLoader) 

200 resultsNoFitRefSelect = solverNoFitRefSelect.run( 

201 sourceCat=sourceCat, 

202 exposure=self.exposure, 

203 ) 

204 self.assertLess(len(resultsNoFitRefSelect.matches), len(resultsNoFit.matches)) 

205 

206 def makeSourceCat(self, wcs, sourceSchema=None, doScatterCentroids=False): 

207 """Make a source catalog by reading the position reference stars using 

208 the proviced WCS. 

209 

210 Optionally provide a schema for the source catalog (to allow 

211 AstrometryTask in the test methods to update it with the 

212 "calib_astrometry_used" flag). Otherwise, a minimal SourceTable 

213 schema will be created. 

214 

215 Optionally, via doScatterCentroids, add some scatter to the centroids 

216 assiged to the source catalog (otherwise they will be identical to 

217 those of the reference catalog). 

218 """ 

219 loadRes = self.refObjLoader.loadPixelBox(bbox=self.bbox, wcs=wcs, filterName="r") 

220 refCat = loadRes.refCat 

221 

222 if sourceSchema is None: 

223 sourceSchema = afwTable.SourceTable.makeMinimalSchema() 

224 measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema 

225 sourceCat = afwTable.SourceCatalog(sourceSchema) 

226 

227 sourceCat.resize(len(refCat)) 

228 scatterFactor = 1.0 

229 if doScatterCentroids: 

230 np.random.seed(12345) 

231 scatterFactor = np.random.uniform(0.999, 1.001, len(sourceCat)) 

232 sourceCat["slot_Centroid_x"] = scatterFactor*refCat["centroid_x"] 

233 sourceCat["slot_Centroid_y"] = scatterFactor*refCat["centroid_y"] 

234 sourceCat["slot_ApFlux_instFlux"] = refCat["r_flux"] 

235 sourceCat["slot_ApFlux_instFluxErr"] = refCat["r_flux"]/100 

236 

237 # Deliberately add some outliers to check that the magnitude 

238 # outlier rejection code is being run. 

239 sourceCat["slot_ApFlux_instFlux"][0: 4] *= 1000.0 

240 

241 return sourceCat 

242 

243 

244class TestMagnitudeOutliers(lsst.utils.tests.TestCase): 

245 def testMagnitudeOutlierRejection(self): 

246 """Test rejection of magnitude outliers. 

247 

248 This test only tests the outlier rejection, and not any other 

249 part of the matching or astrometry fitter. 

250 """ 

251 config = AstrometryTask.ConfigClass() 

252 config.doMagnitudeOutlierRejection = True 

253 config.magnitudeOutlierRejectionNSigma = 4.0 

254 solver = AstrometryTask(config=config, refObjLoader=None) 

255 

256 nTest = 100 

257 

258 refSchema = lsst.afw.table.SimpleTable.makeMinimalSchema() 

259 refSchema.addField('refFlux', 'F') 

260 refCat = lsst.afw.table.SimpleCatalog(refSchema) 

261 refCat.resize(nTest) 

262 

263 srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema() 

264 srcSchema.addField('srcFlux', 'F') 

265 srcCat = lsst.afw.table.SourceCatalog(srcSchema) 

266 srcCat.resize(nTest) 

267 

268 np.random.seed(12345) 

269 refMag = np.full(nTest, 20.0) 

270 srcMag = np.random.normal(size=nTest, loc=0.0, scale=1.0) 

271 

272 # Determine the sigma of the random sample 

273 zp = np.median(refMag[: -4] - srcMag[: -4]) 

274 sigma = scipy.stats.median_abs_deviation(srcMag[: -4], scale='normal') 

275 

276 # Deliberately alter some magnitudes to be outliers. 

277 srcMag[-3] = (config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp) 

278 srcMag[-4] = -(config.magnitudeOutlierRejectionNSigma + 0.1)*sigma + (20.0 - zp) 

279 

280 refCat['refFlux'] = (refMag*units.ABmag).to_value(units.nJy) 

281 srcCat['srcFlux'] = 10.0**(srcMag/(-2.5)) 

282 

283 # Deliberately poison some reference fluxes. 

284 refCat['refFlux'][-1] = np.inf 

285 refCat['refFlux'][-2] = np.nan 

286 

287 matchesIn = [] 

288 for ref, src in zip(refCat, srcCat): 

289 matchesIn.append(lsst.afw.table.ReferenceMatch(first=ref, second=src, distance=0.0)) 

290 

291 matchesOut = solver._removeMagnitudeOutliers('srcFlux', 'refFlux', matchesIn) 

292 

293 # We should lose the 4 outliers we created. 

294 self.assertEqual(len(matchesOut), len(matchesIn) - 4) 

295 

296 

297class MemoryTester(lsst.utils.tests.MemoryTestCase): 

298 pass 

299 

300 

301def setup_module(module): 

302 lsst.utils.tests.init() 

303 

304 

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

306 lsst.utils.tests.init() 

307 unittest.main()