Coverage for tests/test_astrometryTask.py: 16%

181 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-04 02:56 -0700

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 logging 

24import os.path 

25import math 

26import unittest 

27import glob 

28 

29from astropy import units 

30import scipy.stats 

31import numpy as np 

32 

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 

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

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

133 

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) 

153 

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

164 

165 angSep = refCoord.separation(srcCoord) 

166 maxAngSep = max(maxAngSep, angSep) 

167 

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) 

174 

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

185 

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 

196 

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) 

207 

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) 

214 

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

224 

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. 

228 

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. 

233 

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 

240 

241 if sourceSchema is None: 

242 sourceSchema = afwTable.SourceTable.makeMinimalSchema() 

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

244 sourceCat = afwTable.SourceCatalog(sourceSchema) 

245 

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 

255 

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 

259 

260 return sourceCat 

261 

262 

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

264 def testMagnitudeOutlierRejection(self): 

265 """Test rejection of magnitude outliers. 

266 

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) 

274 

275 nTest = 100 

276 

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

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

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

280 refCat.resize(nTest) 

281 

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

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

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

285 srcCat.resize(nTest) 

286 

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) 

290 

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

294 

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) 

298 

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

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

301 

302 # Deliberately poison some reference fluxes. 

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

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

305 

306 matchesIn = [] 

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

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

309 

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

311 

312 # We should lose the 4 outliers we created. 

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

314 

315 

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

317 pass 

318 

319 

320def setup_module(module): 

321 lsst.utils.tests.init() 

322 

323 

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