Coverage for tests/test_astrometryTask.py: 17%

186 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 03:19 -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 schema = self._makeSourceCatalogSchema() 

85 # schema must be passed to the solver task constructor 

86 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=schema) 

87 sourceCat = self.makeSourceCat(self.tanWcs, schema) 

88 

89 results = solver.run( 

90 sourceCat=sourceCat, 

91 exposure=self.exposure, 

92 ) 

93 # check that the used flag is set the right number of times 

94 count = 0 

95 for source in sourceCat: 

96 if source.get('calib_astrometry_used'): 

97 count += 1 

98 self.assertEqual(count, len(results.matches)) 

99 

100 def testWcsFailure(self): 

101 """In the case of a failed WCS fit, test that the exposure's WCS is set 

102 to None and the coord_ra & coord_dec columns are set to nan in the 

103 source catalog. 

104 """ 

105 self.exposure.setWcs(self.tanWcs) 

106 config = AstrometryTask.ConfigClass() 

107 config.wcsFitter.order = 2 

108 config.wcsFitter.maxScatterArcsec = 0.0 # To ensure a WCS failure 

109 

110 schema = self._makeSourceCatalogSchema() 

111 # schema must be passed to the solver task constructor 

112 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=schema) 

113 sourceCat = self.makeSourceCat(self.tanWcs, schema, doScatterCentroids=True) 

114 

115 with self.assertLogs(level=logging.WARNING) as cm: 

116 results = solver.run( 

117 sourceCat=sourceCat, 

118 exposure=self.exposure, 

119 ) 

120 logOutput = ";".join(cm.output) 

121 self.assertIn("WCS fit failed.", logOutput) 

122 self.assertIn("Setting exposure's WCS to None and coord_ra & coord_dec cols in sourceCat to nan.", 

123 logOutput) 

124 # Check that matches is set to None, the sourceCat coord cols are all 

125 # set to nan and that the WCS attached to the exposure is set to None. 

126 self.assertTrue(results.matches is None) 

127 self.assertTrue(np.all(np.isnan(sourceCat["coord_ra"]))) 

128 self.assertTrue(np.all(np.isnan(sourceCat["coord_dec"]))) 

129 self.assertTrue(self.exposure.getWcs() is None) 

130 self.assertTrue(results.scatterOnSky is None) 

131 self.assertTrue(results.matches is None) 

132 

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

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

135 """ 

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

137 modifyActualPixels=False) 

138 self.exposure.setWcs(distortedWcs) 

139 sourceCat = self.makeSourceCat(distortedWcs, self._makeSourceCatalogSchema()) 

140 config = AstrometryTask.ConfigClass() 

141 config.wcsFitter.order = order 

142 config.wcsFitter.numRejIter = 0 

143 # This test is from before rough magnitude rejection was implemented. 

144 config.doMagnitudeOutlierRejection = False 

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

146 results = solver.run( 

147 sourceCat=sourceCat, 

148 exposure=self.exposure, 

149 ) 

150 fitWcs = self.exposure.getWcs() 

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

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

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

154 

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

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

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

158 maxAngSep = 0*lsst.geom.radians 

159 maxPixSep = 0 

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

161 refCoord = refObj.get(refCoordKey) 

162 refPixPos = refObj.get(refCentroidKey) 

163 srcCoord = src.get(srcCoordKey) 

164 srcPixPos = src.getCentroid() 

165 

166 angSep = refCoord.separation(srcCoord) 

167 maxAngSep = max(maxAngSep, angSep) 

168 

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

170 maxPixSep = max(maxPixSep, pixSep) 

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

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

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

174 self.assertLess(maxPixSep, 0.021) 

175 

176 # try again, invoking the reference selector 

177 config.referenceSelector.doUnresolved = True 

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

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

180 self.exposure.setWcs(distortedWcs) 

181 resultsRefSelect = solverRefSelect.run( 

182 sourceCat=sourceCat, 

183 exposure=self.exposure, 

184 ) 

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

186 

187 # try again, allowing magnitude outlier rejection. 

188 config.doMagnitudeOutlierRejection = True 

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

190 self.exposure.setWcs(distortedWcs) 

191 resultsMagOutlierRejection = solverMagOutlierRejection.run( 

192 sourceCat=sourceCat, 

193 exposure=self.exposure, 

194 ) 

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

196 config.doMagnitudeOutlierRejection = False 

197 

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

199 config.referenceSelector.doUnresolved = False 

200 config.forceKnownWcs = True 

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

202 self.exposure.setWcs(distortedWcs) 

203 resultsNoFit = solverNoFit.run( 

204 sourceCat=sourceCat, 

205 exposure=self.exposure, 

206 ) 

207 self.assertIsNone(resultsNoFit.scatterOnSky) 

208 

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

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

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

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

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

214 self.assertLessEqual(meanFitDist, meanNoFitDist) 

215 

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

217 # (this goes through a different code path) 

218 config.referenceSelector.doUnresolved = True 

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

220 resultsNoFitRefSelect = solverNoFitRefSelect.run( 

221 sourceCat=sourceCat, 

222 exposure=self.exposure, 

223 ) 

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

225 

226 @staticmethod 

227 def _makeSourceCatalogSchema(): 

228 """Return a catalog schema with all necessary fields added. 

229 """ 

230 schema = afwTable.SourceTable.makeMinimalSchema() 

231 measBase.SingleFrameMeasurementTask(schema=schema) # expand the schema 

232 afwTable.CoordKey.addErrorFields(schema) 

233 schema.addField("deblend_nChild", type=np.int32, 

234 doc="Number of children this object has (defaults to 0)") 

235 schema.addField("detect_isPrimary", type=np.int32, 

236 doc="true if source has no children and is not a sky source") 

237 return schema 

238 

239 def makeSourceCat(self, wcs, schema, doScatterCentroids=False): 

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

241 the proviced WCS. 

242 

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

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

245 those of the reference catalog). 

246 """ 

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

248 refCat = loadRes.refCat 

249 

250 sourceCat = afwTable.SourceCatalog(schema) 

251 

252 sourceCat.resize(len(refCat)) 

253 scatterFactor = 1.0 

254 if doScatterCentroids: 

255 np.random.seed(12345) 

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

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

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

259 sourceCat["slot_PsfFlux_instFlux"] = refCat["r_flux"] 

260 sourceCat["slot_PsfFlux_instFluxErr"] = refCat["r_flux"]/100 

261 # All of these sources are primary. 

262 sourceCat['detect_isPrimary'] = 1 

263 

264 # Deliberately add some outliers to check that the magnitude 

265 # outlier rejection code is being run. 

266 sourceCat["slot_PsfFlux_instFlux"][0: 4] *= 1000.0 

267 

268 return sourceCat 

269 

270 

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

272 def testMagnitudeOutlierRejection(self): 

273 """Test rejection of magnitude outliers. 

274 

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

276 part of the matching or astrometry fitter. 

277 """ 

278 config = AstrometryTask.ConfigClass() 

279 config.doMagnitudeOutlierRejection = True 

280 config.magnitudeOutlierRejectionNSigma = 4.0 

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

282 

283 nTest = 100 

284 

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

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

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

288 refCat.resize(nTest) 

289 

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

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

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

293 srcCat.resize(nTest) 

294 

295 np.random.seed(12345) 

296 refMag = np.full(nTest, 20.0) 

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

298 

299 # Determine the sigma of the random sample 

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

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

302 

303 # Deliberately alter some magnitudes to be outliers. 

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

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

306 

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

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

309 

310 # Deliberately poison some reference fluxes. 

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

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

313 

314 matchesIn = [] 

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

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

317 

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

319 

320 # We should lose the 4 outliers we created. 

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

322 

323 

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

325 pass 

326 

327 

328def setup_module(module): 

329 lsst.utils.tests.init() 

330 

331 

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

333 lsst.utils.tests.init() 

334 unittest.main()