Hide keyboard shortcuts

Hot-keys 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

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 

26 

27from astropy import units 

28import scipy.stats 

29import numpy as np 

30 

31import lsst.utils.tests 

32import lsst.geom 

33import lsst.afw.geom as afwGeom 

34import lsst.afw.table as afwTable 

35import lsst.afw.image as afwImage 

36import lsst.meas.base as measBase 

37from lsst.daf.persistence import Butler 

38from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask 

39from lsst.meas.astrom import AstrometryTask 

40 

41 

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

43 

44 def setUp(self): 

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

46 

47 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(3001, 3001)) 

48 crpix = lsst.geom.Box2D(self.bbox).getCenter() 

49 self.tanWcs = afwGeom.makeSkyWcs(crpix=crpix, 

50 crval=lsst.geom.SpherePoint(215.5, 53.0, lsst.geom.degrees), 

51 cdMatrix=afwGeom.makeCdMatrix(scale=5.1e-5*lsst.geom.degrees)) 

52 self.exposure = afwImage.ExposureF(self.bbox) 

53 self.exposure.setWcs(self.tanWcs) 

54 self.exposure.setFilterLabel(afwImage.FilterLabel(band="r", physical="rTest")) 

55 butler = Butler(refCatDir) 

56 self.refObjLoader = LoadIndexedReferenceObjectsTask(butler=butler) 

57 

58 def tearDown(self): 

59 del self.tanWcs 

60 del self.exposure 

61 del self.refObjLoader 

62 

63 def testTrivial(self): 

64 """Test fit with no distortion 

65 """ 

66 self.doTest(afwGeom.makeIdentityTransform()) 

67 

68 def testRadial(self): 

69 """Test fit with radial distortion 

70 

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

72 """ 

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

74 

75 def testUsedFlag(self): 

76 """Test that the solver will record number of sources used to table 

77 if it is passed a schema on initialization. 

78 """ 

79 self.exposure.setWcs(self.tanWcs) 

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

81 refCat = loadRes.refCat 

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

83 refFluxRKey = refCat.schema["r_flux"].asKey() 

84 

85 sourceSchema = afwTable.SourceTable.makeMinimalSchema() 

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

87 config = AstrometryTask.ConfigClass() 

88 config.wcsFitter.order = 2 

89 config.wcsFitter.numRejIter = 0 

90 # schema must be passed to the solver task constructor 

91 solver = AstrometryTask(config=config, refObjLoader=self.refObjLoader, schema=sourceSchema) 

92 sourceCat = afwTable.SourceCatalog(sourceSchema) 

93 sourceCat.reserve(len(refCat)) 

94 sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) 

95 sourceInstFluxKey = sourceSchema["slot_ApFlux_instFlux"].asKey() 

96 sourceInstFluxErrKey = sourceSchema["slot_ApFlux_instFluxErr"].asKey() 

97 

98 for refObj in refCat: 

99 src = sourceCat.addNew() 

100 src.set(sourceCentroidKey, refObj.get(refCentroidKey)) 

101 src.set(sourceInstFluxKey, refObj.get(refFluxRKey)) 

102 src.set(sourceInstFluxErrKey, refObj.get(refFluxRKey)/100) 

103 

104 results = solver.run( 

105 sourceCat=sourceCat, 

106 exposure=self.exposure, 

107 ) 

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

109 count = 0 

110 for source in sourceCat: 

111 if source.get('calib_astrometry_used'): 

112 count += 1 

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

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, distortedWcs): 

207 """Make a source catalog by reading the position reference stars and distorting the positions 

208 """ 

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

210 refCat = loadRes.refCat 

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

212 refFluxRKey = refCat.schema["r_flux"].asKey() 

213 

214 sourceSchema = afwTable.SourceTable.makeMinimalSchema() 

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

216 sourceCat = afwTable.SourceCatalog(sourceSchema) 

217 sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) 

218 sourceInstFluxKey = sourceSchema["slot_ApFlux_instFlux"].asKey() 

219 sourceInstFluxErrKey = sourceSchema["slot_ApFlux_instFluxErr"].asKey() 

220 

221 sourceCat.reserve(len(refCat)) 

222 for refObj in refCat: 

223 src = sourceCat.addNew() 

224 src.set(sourceCentroidKey, refObj.get(refCentroidKey)) 

225 src.set(sourceInstFluxKey, refObj.get(refFluxRKey)) 

226 src.set(sourceInstFluxErrKey, refObj.get(refFluxRKey)/100) 

227 

228 # Deliberately add some outliers to check that the magnitude 

229 # outlier rejection code is being run. 

230 sourceCat[sourceInstFluxKey][0: 4] *= 1000.0 

231 

232 return sourceCat 

233 

234 

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

236 def testMagnitudeOutlierRejection(self): 

237 """Test rejection of magnitude outliers. 

238 

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

240 part of the matching or astrometry fitter. 

241 """ 

242 config = AstrometryTask.ConfigClass() 

243 config.doMagnitudeOutlierRejection = True 

244 config.magnitudeOutlierRejectionNSigma = 4.0 

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

246 

247 nTest = 100 

248 

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

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

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

252 refCat.resize(nTest) 

253 

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

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

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

257 srcCat.resize(nTest) 

258 

259 np.random.seed(12345) 

260 refMag = np.full(nTest, 20.0) 

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

262 

263 # Determine the sigma of the random sample 

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

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

266 

267 # Deliberately alter some magnitudes to be outliers. 

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

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

270 

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

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

273 

274 # Deliberately poison some reference fluxes. 

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

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

277 

278 matchesIn = [] 

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

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

281 

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

283 

284 # We should lose the 4 outliers we created. 

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

286 

287 

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

289 pass 

290 

291 

292def setup_module(module): 

293 lsst.utils.tests.init() 

294 

295 

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

297 lsst.utils.tests.init() 

298 unittest.main()