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

1import unittest 

2 

3from astropy.coordinates import SkyCoord 

4 

5import astshim as ast 

6from lsst.geom import arcseconds, degrees, radians, Point2D, SpherePoint 

7from lsst.afw.geom import makeCdMatrix 

8from lsst.afw.geom.detail import readFitsWcs, readLsstSkyWcs, getPropertyListFromFitsChan 

9from lsst.afw.geom.wcsUtils import makeSimpleWcsMetadata 

10import lsst.utils.tests 

11 

12PrintStrippedNames = False 

13 

14 

15class FrameSetUtilsTestCase(lsst.utils.tests.TestCase): 

16 """This is sparse because SkyWcs unit tests test much of this package 

17 """ 

18 

19 def setUp(self): 

20 # arbitrary values 

21 self.crpix = Point2D(100, 100) 

22 self.crval = SpherePoint(30 * degrees, 45 * degrees) 

23 self.scale = 1.0 * arcseconds 

24 

25 def makeMetadata(self): 

26 """Return a WCS that is typical for an image 

27 

28 It will contain 14 cards that describe a WCS, added by makeSimpleWcsMetadata, 

29 plus 25 additional cards, including a set for WCS "A", which is what 

30 LSST uses to store xy0, and LTV1 and LTV2, which LSST used to use for xy0 

31 """ 

32 # arbitrary values 

33 orientation = 0 * degrees 

34 flipX = False 

35 metadata = makeSimpleWcsMetadata( 

36 crpix=self.crpix, 

37 crval=self.crval, 

38 cdMatrix=makeCdMatrix(scale=self.scale, orientation=orientation, flipX=flipX), 

39 ) 

40 self.assertEqual(metadata.nameCount(), 11) # 2 CD terms are zero and so are omitted 

41 metadata.add("SIMPLE", True) 

42 metadata.add("BITPIX", 16) 

43 metadata.add("NAXIS", 2) 

44 metadata.add("NAXIS1", 500) 

45 metadata.add("NAXIS2", 200) 

46 metadata.add("BZERO", 32768) 

47 metadata.add("BSCALE", 1) 

48 metadata.add("TIMESYS", "UTC") 

49 metadata.add("UTC-OBS", "12:04:45.73") 

50 metadata.add("DATE-OBS", "2006-05-20") 

51 metadata.add("EXPTIME", 5.0) 

52 metadata.add("COMMENT", "a comment") 

53 metadata.add("COMMENT", "another comment") 

54 metadata.add("HISTORY", "some history") 

55 metadata.add("EXTEND", True) 

56 metadata.add("INHERIT", False) 

57 metadata.add("CRPIX1A", 1.0) 

58 metadata.add("CRPIX2A", 1.0) 

59 metadata.add("CRVAL1A", 300) 

60 metadata.add("CRVAL2A", 400) 

61 metadata.add("CUNIT1A", "pixels") 

62 metadata.add("CUNIT2A", "pixels") 

63 metadata.add("LTV1", -300) 

64 metadata.add("LTV2", -400) 

65 metadata.add("ZOTHER", "non-standard") 

66 return metadata 

67 

68 def getCrpix(self, metadata): 

69 """Get CRPIX from metadata using the LSST convention: 0-based in parent coordinates 

70 """ 

71 return Point2D( # zero-based, hence the - 1 

72 metadata.getScalar("CRPIX1") + metadata.getScalar("CRVAL1A") - 1, 

73 metadata.getScalar("CRPIX2") + metadata.getScalar("CRVAL2A") - 1, 

74 ) 

75 

76 def testIgnoreNan(self): 

77 """Test that NaN in a property list does not crash the WCS extraction. 

78 """ 

79 metadata = self.makeMetadata() 

80 metadata["ISNAN"] = float("NaN") 

81 

82 # This will issue a warning from C++ but that warning can not be 

83 # redirected to python logging machinery. 

84 # This code causes a SIGABRT without DM-21355 implemented 

85 frameSet1 = readFitsWcs(metadata, strip=False) 

86 self.assertEqual(type(frameSet1), ast.FrameSet) 

87 

88 def testReadFitsWcsStripMetadata(self): 

89 metadata = self.makeMetadata() 

90 nKeys = len(metadata.toList()) 

91 nToStrip = 11 

92 frameSet1 = readFitsWcs(metadata, strip=False) 

93 self.assertEqual(type(frameSet1), ast.FrameSet) 

94 self.assertEqual(len(metadata.toList()), nKeys) 

95 

96 # read again, this time stripping metadata 

97 frameSet2 = readFitsWcs(metadata, strip=True) 

98 self.assertEqual(len(metadata.toList()), nKeys - nToStrip) 

99 self.assertEqual(frameSet1, frameSet2) 

100 

101 # having stripped the metadata, it should not be possible to create a WCS 

102 with self.assertRaises(lsst.pex.exceptions.TypeError): 

103 readFitsWcs(metadata, strip=False) 

104 

105 def testReadFitsWcsFixRadecsys(self): 

106 # compare a WCS made with RADESYS against one made with RADECSYS, 

107 # using a system other than FK5, since that is the default; 

108 # both should be the same because readFitsWcs replaces RADECSYS with RADESYS 

109 metadata1 = self.makeMetadata() 

110 metadata1.set("RADESYS", "ICRS") 

111 frameSet1 = readFitsWcs(metadata1, strip=False) 

112 self.assertEqual(metadata1.getScalar("RADESYS"), "ICRS") 

113 

114 metadata2 = self.makeMetadata() 

115 metadata2.remove("RADESYS") 

116 metadata2.set("RADECSYS", "ICRS") 

117 frameSet2 = readFitsWcs(metadata2, strip=False) 

118 # metadata will have been corrected by readFitsWcs 

119 self.assertFalse(metadata2.exists("RADECSYS")) 

120 self.assertEqual(metadata2.getScalar("RADESYS"), "ICRS") 

121 self.assertEqual(frameSet1, frameSet2) 

122 

123 def testReadLsstSkyWcsStripMetadata(self): 

124 metadata = self.makeMetadata() 

125 nKeys = len(metadata.toList()) 

126 nToStrip = 11 + 6 # WCS "A" is also stripped 

127 frameSet1 = readLsstSkyWcs(metadata, strip=False) 

128 self.assertEqual(len(metadata.toList()), nKeys) 

129 

130 crval = SpherePoint(metadata.getScalar("CRVAL1"), metadata.getScalar("CRVAL2"), degrees) 

131 crvalRad = crval.getPosition(radians) 

132 desiredCrpix = self.getCrpix(metadata) 

133 computedCrpix = frameSet1.applyInverse(crvalRad) 

134 self.assertPairsAlmostEqual(desiredCrpix, computedCrpix) 

135 

136 # read again, this time stripping metadata 

137 frameSet2 = readLsstSkyWcs(metadata, strip=True) 

138 self.assertEqual(len(metadata.toList()), nKeys - nToStrip) 

139 self.assertEqual(frameSet1, frameSet2) 

140 

141 # having stripped the WCS keywords, we should not be able to generate 

142 # a WCS from what's left 

143 with self.assertRaises(lsst.pex.exceptions.TypeError): 

144 readLsstSkyWcs(metadata, strip=False) 

145 

146 # try a full WCS with just CRPIX1 or 2 missing 

147 for i in (1, 2): 

148 metadata = self.makeMetadata() 

149 metadata.remove(f"CRPIX{i}") 

150 with self.assertRaises(lsst.pex.exceptions.TypeError): 

151 readLsstSkyWcs(metadata, strip=False) 

152 

153 def testReadLsstSkyWcsNormalizeFk5(self): 

154 """Test that readLsstSkyWcs correctly normalizes FK5 1975 to ICRS 

155 """ 

156 equinox = 1975 

157 metadata = self.makeMetadata() 

158 metadata.set("RADESYS", "FK5") 

159 metadata.set("EQUINOX", equinox) 

160 crpix = self.getCrpix(metadata) 

161 # record the original CRVAL before reading and stripping metadata 

162 crvalFk5Deg = (metadata.getScalar("CRVAL1"), metadata.getScalar("CRVAL2")) 

163 

164 # read frameSet and compute crval 

165 frameSet = readLsstSkyWcs(metadata, strip=True) 

166 crvalRadians = frameSet.applyForward(crpix) 

167 crvalPoint = SpherePoint(*[crvalRadians[i]*radians for i in range(2)]) 

168 

169 # compare crval to the value recorded as the skyRef property of the current frame 

170 skyFrame = frameSet.getFrame(ast.FrameSet.CURRENT) 

171 recordedCrvalRadians = skyFrame.getSkyRef() 

172 recordedCrvalPoint = SpherePoint(*[recordedCrvalRadians[i]*radians for i in range(2)]) 

173 self.assertSpherePointsAlmostEqual(crvalPoint, recordedCrvalPoint) 

174 

175 # get predicted crval by converting with astropy 

176 crvalFk5 = SkyCoord(crvalFk5Deg[0], crvalFk5Deg[1], frame="fk5", 

177 equinox=f"J{equinox}", unit="deg") 

178 predictedCrvalIcrs = crvalFk5.icrs 

179 predictedCrvalPoint = SpherePoint(predictedCrvalIcrs.ra.radian*radians, 

180 predictedCrvalIcrs.dec.radian*radians) 

181 # AST and astropy disagree by 0.025 arcsec; it's not worth worrying about because 

182 # we will always use ICRS internally and almost always fit our own WCS. 

183 self.assertSpherePointsAlmostEqual(crvalPoint, predictedCrvalPoint, maxSep=0.05*arcseconds) 

184 

185 def testReadLsstSkyWcsNormalizeRaDec(self): 

186 """Test that a Dec, RA WCS frame set is normalized to RA, Dec 

187 """ 

188 metadata = self.makeMetadata() 

189 

190 crpix = self.getCrpix(metadata) 

191 

192 # swap RA, Decaxes in metadata 

193 crvalIn = SpherePoint(metadata.getScalar("CRVAL1"), metadata.getScalar("CRVAL2"), degrees) 

194 metadata.set("CRVAL1", crvalIn[1].asDegrees()) 

195 metadata.set("CRVAL2", crvalIn[0].asDegrees()) 

196 metadata.set("CTYPE1", "DEC--TAN") 

197 metadata.set("CTYPE2", "RA---TAN") 

198 

199 # create the wcs 

200 frameSet = readLsstSkyWcs(metadata) 

201 

202 # compute pixel origin and compare to input crval 

203 computedCrvalRadians = frameSet.applyForward(crpix) 

204 computedCrval = SpherePoint(*[computedCrvalRadians[i]*radians for i in range(2)]) 

205 self.assertSpherePointsAlmostEqual(crvalIn, computedCrval) 

206 

207 def testGetPropertyListFromFitsChanWithComments(self): 

208 fc = ast.FitsChan(ast.StringStream()) 

209 self.assertEqual(fc.className, "FitsChan") 

210 

211 # add one card for each supported type, with a comment 

212 continueVal = "This is a continue card" 

213 floatVal = 1.5 

214 intVal = 99 

215 logicalVal = True 

216 stringVal = "This is a string" 

217 fc.setFitsCN("ACONT", continueVal, "Comment for ACONT") 

218 fc.setFitsF("AFLOAT", floatVal, "Comment for AFLOAT") 

219 fc.setFitsI("ANINT", intVal, "Comment for ANINT") 

220 fc.setFitsL("ALOGICAL", logicalVal, "Comment for ALOGICAL") 

221 fc.setFitsS("ASTRING", stringVal, "Comment for ASTRING") 

222 fc.setFitsCM("a comment, which will be ignored by getPropertyListFromFitsChan") 

223 fc.setFitsU("ANUNDEF", "Comment for ANUNDEF") 

224 expectedNames = ["ACONT", "AFLOAT", "ANINT", "ALOGICAL", "ASTRING", "ANUNDEF"] 

225 

226 self.assertEqual(fc.nCard, 7) 

227 metadata = getPropertyListFromFitsChan(fc) 

228 self.assertEqual(metadata.getOrderedNames(), expectedNames) 

229 

230 self.assertEqual(metadata.getScalar("ACONT"), continueVal) 

231 self.assertAlmostEqual(metadata.getScalar("AFLOAT"), floatVal) 

232 self.assertEqual(metadata.getScalar("ANINT"), intVal) 

233 self.assertEqual(metadata.getScalar("ALOGICAL"), logicalVal) 

234 self.assertEqual(metadata.getScalar("ASTRING"), stringVal) 

235 self.assertEqual(metadata.getScalar("ACONT"), continueVal) 

236 self.assertIsNone(metadata.getScalar("ANUNDEF")) 

237 

238 for name in expectedNames: 

239 self.assertEqual(metadata.getComment(name), f"Comment for {name}") 

240 

241 # complex values are not supported by PropertyList 

242 fc.setFitsCF("UNSUPP", complex(1, 0)) 

243 badCard = fc.getCard() 

244 with self.assertRaises(lsst.pex.exceptions.TypeError): 

245 getPropertyListFromFitsChan(fc) 

246 fc.setCard(badCard) 

247 fc.clearCard() 

248 fc.findFits("UNSUPP", inc=False) 

249 fc.delFits() 

250 metadata = getPropertyListFromFitsChan(fc) 

251 self.assertEqual(metadata.getOrderedNames(), expectedNames) 

252 

253 def testGetFitsCardsNoComments(self): 

254 fc = ast.FitsChan(ast.StringStream()) 

255 self.assertEqual(fc.className, "FitsChan") 

256 

257 # add one card for each supported type, with a comment 

258 continueVal = "This is a continue card" 

259 floatVal = 1.5 

260 intVal = 99 

261 logicalVal = True 

262 stringVal = "This is a string" 

263 fc.setFitsCN("ACONT", continueVal) 

264 fc.setFitsF("AFLOAT", floatVal) 

265 fc.setFitsI("ANINT", intVal) 

266 fc.setFitsL("ALOGICAL", logicalVal) 

267 fc.setFitsS("ASTRING", stringVal) 

268 fc.setFitsCM("a comment, which will be ignored by getPropertyListFromFitsChan") 

269 

270 self.assertEqual(fc.nCard, 6) 

271 metadata = getPropertyListFromFitsChan(fc) 

272 self.assertEqual(metadata.getOrderedNames(), ["ACONT", "AFLOAT", "ANINT", "ALOGICAL", "ASTRING"]) 

273 

274 self.assertEqual(metadata.getScalar("ACONT"), continueVal) 

275 self.assertAlmostEqual(metadata.getScalar("AFLOAT"), floatVal) 

276 self.assertEqual(metadata.getScalar("ANINT"), intVal) 

277 self.assertEqual(metadata.getScalar("ALOGICAL"), logicalVal) 

278 self.assertEqual(metadata.getScalar("ASTRING"), stringVal) 

279 self.assertEqual(metadata.getScalar("ACONT"), continueVal) 

280 

281 def testGetPropertyListFromFitsChanUnsupportedTypes(self): 

282 fc = ast.FitsChan(ast.StringStream()) 

283 self.assertEqual(fc.className, "FitsChan") 

284 fc.setFitsCF("ACOMPLEX", complex(1, 1)) 

285 with self.assertRaises(lsst.pex.exceptions.TypeError): 

286 getPropertyListFromFitsChan(fc) 

287 

288 def testMakeSimpleWcsMetadata(self): 

289 crpix = Point2D(111.1, 222.2) 

290 crval = SpherePoint(45.6 * degrees, 12.3 * degrees) 

291 scale = 1 * arcseconds 

292 for orientation in (0 * degrees, 21 * degrees): 

293 cdMatrix = makeCdMatrix(scale=scale, orientation=orientation) 

294 for projection in ("TAN", "STG"): 

295 metadata = makeSimpleWcsMetadata(crpix=crpix, crval=crval, 

296 cdMatrix=cdMatrix, projection=projection) 

297 desiredLength = 11 if orientation == 0 * degrees else 13 

298 self.assertEqual(len(metadata.names()), desiredLength) 

299 self.assertEqual(metadata.getScalar("RADESYS"), "ICRS") 

300 self.assertFalse(metadata.exists("EQUINOX")) 

301 self.assertEqual(metadata.getScalar("CTYPE1"), "RA---" + projection) 

302 self.assertEqual(metadata.getScalar("CTYPE2"), "DEC--" + projection) 

303 for i in range(2): 

304 self.assertAlmostEqual(metadata.getScalar(f"CRPIX{i + 1}"), crpix[i] + 1) 

305 self.assertAlmostEqual(metadata.getScalar(f"CRVAL{i + 1}"), crval[i].asDegrees()) 

306 self.assertEqual(metadata.getScalar(f"CUNIT{i + 1}"), "deg") 

307 for i in range(2): 

308 for j in range(2): 

309 name = f"CD{i + 1}_{j + 1}" 

310 if cdMatrix[i, j] != 0: 

311 self.assertAlmostEqual(metadata.getScalar(name), cdMatrix[i, j]) 

312 else: 

313 self.assertFalse(metadata.exists(name)) 

314 

315 

316class TestMemory(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()