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

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

91 

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

102 

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) 

134 

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) 

156 

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

167 

168 angSep = refCoord.separation(srcCoord) 

169 maxAngSep = max(maxAngSep, angSep) 

170 

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) 

177 

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

188 

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 

199 

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) 

210 

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) 

217 

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

227 

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. 

231 

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. 

236 

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 

243 

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) 

249 

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 

259 

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 

263 

264 return sourceCat 

265 

266 

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

268 def testMagnitudeOutlierRejection(self): 

269 """Test rejection of magnitude outliers. 

270 

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) 

278 

279 nTest = 100 

280 

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

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

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

284 refCat.resize(nTest) 

285 

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

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

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

289 srcCat.resize(nTest) 

290 

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) 

294 

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

298 

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) 

302 

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

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

305 

306 # Deliberately poison some reference fluxes. 

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

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

309 

310 matchesIn = [] 

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

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

313 

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

315 

316 # We should lose the 4 outliers we created. 

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

318 

319 

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

321 pass 

322 

323 

324def setup_module(module): 

325 lsst.utils.tests.init() 

326 

327 

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