Coverage for tests/test_skyWcs.py: 11%

543 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 03:22 -0800

1import itertools 

2import os 

3import sys 

4import unittest 

5 

6import astropy.io.fits 

7import astropy.coordinates 

8import astropy.wcs 

9import astshim as ast 

10import numpy as np 

11from numpy.testing import assert_allclose 

12 

13import lsst.utils.tests 

14from lsst.daf.base import PropertyList 

15import lsst.geom 

16import lsst.afw.cameraGeom as cameraGeom 

17from lsst.afw.geom import wcsAlmostEqualOverBBox, \ 

18 TransformPoint2ToPoint2, TransformPoint2ToSpherePoint, makeRadialTransform, \ 

19 SkyWcs, makeSkyWcs, makeCdMatrix, makeWcsPairTransform, \ 

20 makeFlippedWcs, makeModifiedWcs, makeTanSipWcs, \ 

21 getIntermediateWorldCoordsToSky, getPixelToIntermediateWorldCoords 

22from lsst.afw.geom import getCdMatrixFromMetadata, getSipMatrixFromMetadata, makeSimpleWcsMetadata 

23from lsst.afw.geom.testUtils import makeSipIwcToPixel, makeSipPixelToIwc 

24from lsst.afw.fits import makeLimitedFitsHeader 

25from lsst.afw.image import ExposureF 

26 

27 

28def addActualPixelsFrame(skyWcs, actualPixelsToPixels): 

29 """Add an "ACTUAL_PIXELS" frame to a SkyWcs and return the result 

30 

31 Parameters 

32 ---------- 

33 skyWcs : `lsst.afw.geom.SkyWcs` 

34 The WCS to which you wish to add an ACTUAL_PIXELS frame 

35 actualPixelsToPixels : `lsst.afw.geom.TransformPoint2ToPoint2` 

36 The transform from ACTUAL_PIXELS to PIXELS 

37 """ 

38 actualPixelsToPixelsMap = actualPixelsToPixels.getMapping() 

39 actualPixelsFrame = ast.Frame(2, "Domain=ACTUAL_PIXELS") 

40 frameDict = skyWcs.getFrameDict() 

41 frameDict.addFrame("PIXELS", actualPixelsToPixelsMap.inverted(), actualPixelsFrame) 

42 frameDict.setBase("ACTUAL_PIXELS") 

43 frameDict.setCurrent("SKY") 

44 return SkyWcs(frameDict) 

45 

46 

47class SkyWcsBaseTestCase(lsst.utils.tests.TestCase): 

48 def checkPersistence(self, skyWcs, bbox): 

49 """Check persistence of a SkyWcs 

50 """ 

51 className = "SkyWcs" 

52 

53 # check writeString and readString 

54 skyWcsStr = skyWcs.writeString() 

55 serialVersion, serialClassName, serialRest = skyWcsStr.split(" ", 2) 

56 self.assertEqual(int(serialVersion), 1) 

57 self.assertEqual(serialClassName, className) 

58 badStr1 = " ".join(["2", serialClassName, serialRest]) 

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

60 skyWcs.readString(badStr1) 

61 badClassName = "x" + serialClassName 

62 badStr2 = " ".join(["1", badClassName, serialRest]) 

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

64 skyWcs.readString(badStr2) 

65 skyWcsFromStr1 = skyWcs.readString(skyWcsStr) 

66 self.assertEqual(skyWcs, skyWcsFromStr1) 

67 self.assertEqual(type(skyWcs), type(skyWcsFromStr1)) 

68 self.assertEqual(skyWcs.getFrameDict(), skyWcsFromStr1.getFrameDict()) 

69 

70 pixelPoints = [ 

71 lsst.geom.Point2D(0, 0), 

72 lsst.geom.Point2D(1000, 0), 

73 lsst.geom.Point2D(0, 1000), 

74 lsst.geom.Point2D(-50, -50), 

75 ] 

76 skyPoints = skyWcs.pixelToSky(pixelPoints) 

77 pixelPoints2 = skyWcs.skyToPixel(skyPoints) 

78 assert_allclose(pixelPoints, pixelPoints2, atol=1e-7) 

79 

80 # check that WCS is properly saved as part of an exposure FITS file 

81 exposure = ExposureF(100, 100, skyWcs) 

82 with lsst.utils.tests.getTempFilePath(".fits") as outFile: 

83 exposure.writeFits(outFile) 

84 exposureRoundTrip = ExposureF(outFile) 

85 wcsFromExposure = exposureRoundTrip.getWcs() 

86 self.assertWcsAlmostEqualOverBBox(skyWcs, wcsFromExposure, bbox, maxDiffPix=0, 

87 maxDiffSky=0*lsst.geom.radians) 

88 

89 def checkFrameDictConstructor(self, skyWcs, bbox): 

90 """Check that the FrameDict constructor works 

91 """ 

92 frameDict = skyWcs.getFrameDict() 

93 wcsFromFrameDict = SkyWcs(frameDict) 

94 self.assertWcsAlmostEqualOverBBox(skyWcs, wcsFromFrameDict, bbox, maxDiffPix=0, 

95 maxDiffSky=0*lsst.geom.radians) 

96 

97 self.checkPersistence(wcsFromFrameDict, bbox) 

98 

99 # check that it is impossible to build a SkyWcs if a required frame is missing 

100 for domain in ("PIXELS", "IWC", "SKY"): 

101 badFrameDict = skyWcs.getFrameDict() 

102 badFrameDict.removeFrame(domain) 

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

104 SkyWcs(badFrameDict) 

105 

106 def checkMakeFlippedWcs(self, skyWcs, skyAtol=1e-7*lsst.geom.arcseconds): 

107 """Check makeFlippedWcs on the provided WCS 

108 """ 

109 # make an arbitrary bbox, but one that includes zero in one axis 

110 # and does not include zero in the other axis 

111 # the center of the bbox is used as the center of flipping 

112 # and the corners of the bbox are the input positions that are tested 

113 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-100, 1000), lsst.geom.Extent2D(2000, 1501)) 

114 # dict of (isRight, isTop): position 

115 minPos = bbox.getMin() 

116 maxPos = bbox.getMax() 

117 center = bbox.getCenter() 

118 cornerDict = { 

119 (False, False): minPos, 

120 (False, True): lsst.geom.Point2D(minPos[0], maxPos[1]), 

121 (True, False): lsst.geom.Point2D(maxPos[0], minPos[1]), 

122 (True, True): maxPos, 

123 } 

124 for flipLR, flipTB in itertools.product((False, True), (False, True)): 

125 flippedWcs = makeFlippedWcs(wcs=skyWcs, flipLR=flipLR, flipTB=flipTB, center=center) 

126 # the center is unchanged 

127 self.assertSpherePointsAlmostEqual(skyWcs.pixelToSky(center), 

128 flippedWcs.pixelToSky(center), maxSep=skyAtol) 

129 

130 for isR, isT in itertools.product((False, True), (False, True)): 

131 origPos = cornerDict[(isR, isT)] 

132 flippedPos = cornerDict[(isR ^ flipLR, isT ^ flipTB)] 

133 self.assertSpherePointsAlmostEqual(skyWcs.pixelToSky(origPos), 

134 flippedWcs.pixelToSky(flippedPos), maxSep=skyAtol) 

135 

136 def assertSkyWcsAstropyWcsAlmostEqual(self, skyWcs, astropyWcs, bbox, 

137 pixAtol=1e-4, skyAtol=1e-4*lsst.geom.arcseconds, 

138 checkRoundTrip=True): 

139 """Assert that a SkyWcs and the corresponding astropy.wcs.WCS agree over a specified bounding box 

140 """ 

141 bbox = lsst.geom.Box2D(bbox) 

142 center = bbox.getCenter() 

143 xArr = bbox.getMinX(), center[0], bbox.getMaxX() 

144 yArr = bbox.getMinY(), center[1], bbox.getMaxY() 

145 pixPosList = [lsst.geom.Point2D(x, y) for x, y in itertools.product(xArr, yArr)] 

146 

147 # pixelToSky 

148 skyPosList = skyWcs.pixelToSky(pixPosList) 

149 astropySkyPosList = self.astropyPixelsToSky(astropyWcs=astropyWcs, pixPosList=pixPosList) 

150 self.assertSpherePointListsAlmostEqual(skyPosList, astropySkyPosList, maxSep=skyAtol) 

151 

152 if not checkRoundTrip: 

153 return 

154 

155 # astropy round trip 

156 astropyPixPosRoundTrip = self.astropySkyToPixels(astropyWcs=astropyWcs, skyPosList=astropySkyPosList) 

157 self.assertPairListsAlmostEqual(pixPosList, astropyPixPosRoundTrip, maxDiff=pixAtol) 

158 

159 # SkyWcs round trip 

160 pixPosListRoundTrip = skyWcs.skyToPixel(skyPosList) 

161 self.assertPairListsAlmostEqual(pixPosList, pixPosListRoundTrip, maxDiff=pixAtol) 

162 

163 # skyToPixel astropy vs SkyWcs 

164 astropyPixPosList2 = self.astropySkyToPixels(astropyWcs=astropyWcs, skyPosList=skyPosList) 

165 self.assertPairListsAlmostEqual(pixPosListRoundTrip, astropyPixPosList2, maxDiff=pixAtol) 

166 

167 def astropyPixelsToSky(self, astropyWcs, pixPosList): 

168 """Use an astropy wcs to convert pixels to sky 

169 

170 @param[in] astropyWcs a celestial astropy.wcs.WCS with 2 axes in RA, Dec order 

171 @param[in] pixPosList 0-based pixel positions as lsst.geom.Point2D or similar pairs 

172 @returns sky coordinates as a list of lsst.geom.SpherePoint 

173 

174 Converts the output to ICRS 

175 """ 

176 xarr = [p[0] for p in pixPosList] 

177 yarr = [p[1] for p in pixPosList] 

178 skyCoordList = astropy.wcs.utils.pixel_to_skycoord(xp=xarr, 

179 yp=yarr, 

180 wcs=astropyWcs, 

181 origin=0, 

182 mode="all") 

183 icrsList = [sc.transform_to("icrs") for sc in skyCoordList] 

184 return [lsst.geom.SpherePoint(sc.ra.deg, sc.dec.deg, lsst.geom.degrees) for sc in icrsList] 

185 

186 def astropySkyToPixels(self, astropyWcs, skyPosList): 

187 """Use an astropy wcs to convert pixels to sky 

188 

189 @param[in] astropyWcs a celestial astropy.wcs.WCS with 2 axes in RA, Dec order 

190 @param[in] skyPosList ICRS sky coordinates as a list of lsst.geom.SpherePoint 

191 @returns a list of lsst.geom.Point2D, 0-based pixel positions 

192 

193 Converts the input from ICRS to the coordinate system of the wcs 

194 """ 

195 skyCoordList = [astropy.coordinates.SkyCoord(c[0].asDegrees(), 

196 c[1].asDegrees(), 

197 frame="icrs", 

198 unit="deg") for c in skyPosList] 

199 xyArr = [astropy.wcs.utils.skycoord_to_pixel(coords=sc, 

200 wcs=astropyWcs, 

201 origin=0, 

202 mode="all") for sc in skyCoordList] 

203 # float is needed to avoid truncation to int 

204 return [lsst.geom.Point2D(float(x), float(y)) for x, y in xyArr] 

205 

206 

207class SimpleSkyWcsTestCase(SkyWcsBaseTestCase): 

208 """Test the simple FITS version of makeSkyWcs 

209 """ 

210 

211 def setUp(self): 

212 self.crpix = lsst.geom.Point2D(100, 100) 

213 self.crvalList = [ 

214 lsst.geom.SpherePoint(0, 45, lsst.geom.degrees), 

215 lsst.geom.SpherePoint(0.00001, 45, lsst.geom.degrees), 

216 lsst.geom.SpherePoint(359.99999, 45, lsst.geom.degrees), 

217 lsst.geom.SpherePoint(30, 89.99999, lsst.geom.degrees), 

218 lsst.geom.SpherePoint(30, -89.99999, lsst.geom.degrees), 

219 ] 

220 self.orientationList = [ 

221 0 * lsst.geom.degrees, 

222 0.00001 * lsst.geom.degrees, 

223 -0.00001 * lsst.geom.degrees, 

224 -45 * lsst.geom.degrees, 

225 90 * lsst.geom.degrees, 

226 ] 

227 self.scale = 1.0 * lsst.geom.arcseconds 

228 self.tinyPixels = 1.0e-10 

229 self.tinyAngle = 1.0e-10 * lsst.geom.radians 

230 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-1000, -1000), 

231 lsst.geom.Extent2I(2000, 2000)) # arbitrary but reasonable 

232 

233 def checkTanWcs(self, crval, orientation, flipX): 

234 """Construct a pure TAN SkyWcs and check that it operates as specified 

235 

236 Parameters 

237 ---------- 

238 crval : `lsst.geom.SpherePoint` 

239 Desired reference sky position. 

240 Must not be at either pole. 

241 orientation : `lsst.geom.Angle` 

242 Position angle of pixel +Y, measured from N through E. 

243 At 0 degrees, +Y is along N and +X is along E/W if flipX false/true 

244 At 90 degrees, +Y is along E and +X is along S/N if flipX false/true 

245 flipX : `bool` 

246 Flip x axis? See `orientation` for details. 

247 

248 Returns 

249 ------- 

250 wcs : `lsst.afw.geom.SkyWcs` 

251 The generated pure TAN SkyWcs 

252 """ 

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

254 wcs = makeSkyWcs(crpix=self.crpix, crval=crval, cdMatrix=cdMatrix) 

255 self.checkPersistence(wcs, bbox=self.bbox) 

256 self.checkMakeFlippedWcs(wcs) 

257 

258 self.assertTrue(wcs.isFits) 

259 self.assertEqual(wcs.isFlipped, bool(flipX)) 

260 

261 xoffAng = 0*lsst.geom.degrees if flipX else 180*lsst.geom.degrees 

262 

263 pixelList = [ 

264 lsst.geom.Point2D(self.crpix[0], self.crpix[1]), 

265 lsst.geom.Point2D(self.crpix[0] + 1, self.crpix[1]), 

266 lsst.geom.Point2D(self.crpix[0], self.crpix[1] + 1), 

267 ] 

268 skyList = wcs.pixelToSky(pixelList) 

269 

270 # check pixels to sky 

271 predSkyList = [ 

272 crval, 

273 crval.offset(xoffAng - orientation, self.scale), 

274 crval.offset(90*lsst.geom.degrees - orientation, self.scale), 

275 ] 

276 self.assertSpherePointListsAlmostEqual(predSkyList, skyList) 

277 self.assertSpherePointListsAlmostEqual(predSkyList, wcs.pixelToSky(pixelList)) 

278 for pixel, predSky in zip(pixelList, predSkyList): 

279 self.assertSpherePointsAlmostEqual(predSky, wcs.pixelToSky(pixel)) 

280 self.assertSpherePointsAlmostEqual(predSky, wcs.pixelToSky(pixel[0], pixel[1])) 

281 

282 # check sky to pixels 

283 self.assertPairListsAlmostEqual(pixelList, wcs.skyToPixel(skyList)) 

284 self.assertPairListsAlmostEqual(pixelList, wcs.skyToPixel(skyList)) 

285 for pixel, sky in zip(pixelList, skyList): 

286 self.assertPairsAlmostEqual(pixel, wcs.skyToPixel(sky)) 

287 # self.assertPairsAlmostEqual(pixel, wcs.skyToPixel(sky[0], sky[1])) 

288 

289 # check CRVAL round trip 

290 self.assertSpherePointsAlmostEqual(wcs.getSkyOrigin(), crval, 

291 maxSep=self.tinyAngle) 

292 

293 crpix = wcs.getPixelOrigin() 

294 self.assertPairsAlmostEqual(crpix, self.crpix, maxDiff=self.tinyPixels) 

295 

296 self.assertFloatsAlmostEqual(wcs.getCdMatrix(), cdMatrix, atol=1e-15, rtol=1e-11) 

297 

298 pixelScale = wcs.getPixelScale() 

299 self.assertAnglesAlmostEqual(self.scale, pixelScale, maxDiff=self.tinyAngle) 

300 

301 pixelScale = wcs.getPixelScale(self.crpix) 

302 self.assertAnglesAlmostEqual(self.scale, pixelScale, maxDiff=self.tinyAngle) 

303 

304 # check that getFitsMetadata can operate at high precision 

305 # and has axis order RA, Dec 

306 fitsMetadata = wcs.getFitsMetadata(True) 

307 self.assertEqual(fitsMetadata.getScalar("CTYPE1")[0:4], "RA--") 

308 self.assertEqual(fitsMetadata.getScalar("CTYPE2")[0:4], "DEC-") 

309 

310 # Compute a WCS with the pixel origin shifted by an arbitrary amount 

311 # The resulting sky origin should not change 

312 offset = lsst.geom.Extent2D(500, -322) # arbitrary 

313 shiftedWcs = wcs.copyAtShiftedPixelOrigin(offset) 

314 self.assertTrue(shiftedWcs.isFits) 

315 predShiftedPixelOrigin = self.crpix + offset 

316 self.assertPairsAlmostEqual(shiftedWcs.getPixelOrigin(), predShiftedPixelOrigin, 

317 maxDiff=self.tinyPixels) 

318 self.assertSpherePointsAlmostEqual(shiftedWcs.getSkyOrigin(), crval, maxSep=self.tinyAngle) 

319 

320 shiftedPixelList = [p + offset for p in pixelList] 

321 shiftedSkyList = shiftedWcs.pixelToSky(shiftedPixelList) 

322 self.assertSpherePointListsAlmostEqual(skyList, shiftedSkyList, maxSep=self.tinyAngle) 

323 

324 # Check that the shifted WCS can be round tripped as FITS metadata 

325 shiftedMetadata = shiftedWcs.getFitsMetadata(precise=True) 

326 shiftedWcsCopy = makeSkyWcs(shiftedMetadata) 

327 shiftedBBox = lsst.geom.Box2D(predShiftedPixelOrigin, 

328 predShiftedPixelOrigin + lsst.geom.Extent2I(2000, 2000)) 

329 self.assertWcsAlmostEqualOverBBox(shiftedWcs, shiftedWcsCopy, shiftedBBox) 

330 

331 wcsCopy = SkyWcs.readString(wcs.writeString()) 

332 self.assertTrue(wcsCopy.isFits) 

333 

334 return wcs 

335 

336 def checkNonFitsWcs(self, wcs): 

337 """Check SkyWcs.getFitsMetadata for a WCS that cannot be represented as a FITS-WCS 

338 """ 

339 # the modified WCS should not be representable as pure FITS-WCS 

340 self.assertFalse(wcs.isFits) 

341 with self.assertRaises(RuntimeError): 

342 wcs.getFitsMetadata(True) 

343 

344 # the approximation returned by getFitsMetadata is poor (pure TAN) until DM-13170 

345 wcsFromMetadata = makeSkyWcs(wcs.getFitsMetadata()) 

346 self.assertFalse(wcsAlmostEqualOverBBox(wcs, wcsFromMetadata, bbox=self.bbox)) 

347 

348 # the approximation returned by getFitsMetadata is pure TAN until DM-13170, 

349 # after DM-13170 change this test to check that wcsFromMetadata is a reasonable 

350 # approximation to the original WCS 

351 approxWcs = wcs.getTanWcs(wcs.getPixelOrigin()) 

352 self.assertWcsAlmostEqualOverBBox(approxWcs, wcsFromMetadata, bbox=self.bbox) 

353 

354 def testTanWcs(self): 

355 """Check a variety of TanWcs, with crval not at a pole. 

356 """ 

357 for crval, orientation, flipX in itertools.product(self.crvalList, 

358 self.orientationList, 

359 (False, True)): 

360 self.checkTanWcs(crval=crval, 

361 orientation=orientation, 

362 flipX=flipX, 

363 ) 

364 

365 def testTanWcsFromFrameDict(self): 

366 """Test making a TAN WCS from a FrameDict 

367 """ 

368 cdMatrix = makeCdMatrix(scale=self.scale) 

369 skyWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

370 self.checkFrameDictConstructor(skyWcs, bbox=self.bbox) 

371 

372 def testGetFrameDict(self): 

373 """Test that getFrameDict returns a deep copy 

374 """ 

375 cdMatrix = makeCdMatrix(scale=self.scale) 

376 skyWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

377 for domain in ("PIXELS", "IWC", "SKY"): 

378 frameDict = skyWcs.getFrameDict() 

379 frameDict.removeFrame(domain) 

380 self.assertFalse(frameDict.hasDomain(domain)) 

381 self.assertTrue(skyWcs.getFrameDict().hasDomain(domain)) 

382 

383 def testMakeModifiedWcsNoActualPixels(self): 

384 """Test makeModifiedWcs on a SkyWcs that has no ACTUAL_PIXELS frame 

385 """ 

386 cdMatrix = makeCdMatrix(scale=self.scale) 

387 originalWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

388 originalFrameDict = originalWcs.getFrameDict() 

389 

390 # make an arbitrary but reasonable transform to insert using makeModifiedWcs 

391 pixelTransform = makeRadialTransform([0.0, 1.0, 0.0, 0.0011]) 

392 # the result of the insertion should be as follows 

393 desiredPixelsToSky = pixelTransform.then(originalWcs.getTransform()) 

394 

395 pixPointList = ( # arbitrary but reasonable 

396 lsst.geom.Point2D(0.0, 0.0), 

397 lsst.geom.Point2D(1000.0, 0.0), 

398 lsst.geom.Point2D(0.0, 2000.0), 

399 lsst.geom.Point2D(-1111.0, -2222.0), 

400 ) 

401 for modifyActualPixels in (False, True): 

402 modifiedWcs = makeModifiedWcs(pixelTransform=pixelTransform, 

403 wcs=originalWcs, 

404 modifyActualPixels=modifyActualPixels) 

405 modifiedFrameDict = modifiedWcs.getFrameDict() 

406 skyList = modifiedWcs.pixelToSky(pixPointList) 

407 

408 # compare pixels to sky 

409 desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) 

410 self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) 

411 

412 # compare pixels to IWC 

413 pixelsToIwc = TransformPoint2ToPoint2(modifiedFrameDict.getMapping("PIXELS", "IWC")) 

414 desiredPixelsToIwc = TransformPoint2ToPoint2( 

415 pixelTransform.getMapping().then(originalFrameDict.getMapping("PIXELS", "IWC"))) 

416 self.assertPairListsAlmostEqual(pixelsToIwc.applyForward(pixPointList), 

417 desiredPixelsToIwc.applyForward(pixPointList)) 

418 

419 self.checkNonFitsWcs(modifiedWcs) 

420 

421 def testMakeModifiedWcsWithActualPixels(self): 

422 """Test makeModifiedWcs on a SkyWcs that has an ACTUAL_PIXELS frame 

423 """ 

424 cdMatrix = makeCdMatrix(scale=self.scale) 

425 baseWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

426 # model actual pixels to pixels as an arbitrary zoom factor; 

427 # this is not realistic, but is fine for a unit test 

428 actualPixelsToPixels = TransformPoint2ToPoint2(ast.ZoomMap(2, 0.72)) 

429 originalWcs = addActualPixelsFrame(baseWcs, actualPixelsToPixels) 

430 originalFrameDict = originalWcs.getFrameDict() 

431 

432 # make an arbitrary but reasonable transform to insert using makeModifiedWcs 

433 pixelTransform = makeRadialTransform([0.0, 1.0, 0.0, 0.0011]) # arbitrary but reasonable 

434 

435 pixPointList = ( # arbitrary but reasonable 

436 lsst.geom.Point2D(0.0, 0.0), 

437 lsst.geom.Point2D(1000.0, 0.0), 

438 lsst.geom.Point2D(0.0, 2000.0), 

439 lsst.geom.Point2D(-1111.0, -2222.0), 

440 ) 

441 for modifyActualPixels in (True, False): 

442 modifiedWcs = makeModifiedWcs(pixelTransform=pixelTransform, 

443 wcs=originalWcs, 

444 modifyActualPixels=modifyActualPixels) 

445 modifiedFrameDict = modifiedWcs.getFrameDict() 

446 self.assertEqual(modifiedFrameDict.getFrame(modifiedFrameDict.BASE).domain, "ACTUAL_PIXELS") 

447 modifiedActualPixelsToPixels = \ 

448 TransformPoint2ToPoint2(modifiedFrameDict.getMapping("ACTUAL_PIXELS", "PIXELS")) 

449 modifiedPixelsToIwc = TransformPoint2ToPoint2(modifiedFrameDict.getMapping("PIXELS", "IWC")) 

450 

451 # compare pixels to sky 

452 skyList = modifiedWcs.pixelToSky(pixPointList) 

453 if modifyActualPixels: 

454 desiredPixelsToSky = pixelTransform.then(originalWcs.getTransform()) 

455 else: 

456 originalPixelsToSky = \ 

457 TransformPoint2ToSpherePoint(originalFrameDict.getMapping("PIXELS", "SKY")) 

458 desiredPixelsToSky = actualPixelsToPixels.then(pixelTransform).then(originalPixelsToSky) 

459 desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) 

460 self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) 

461 

462 # compare ACTUAL_PIXELS to PIXELS and PIXELS to IWC 

463 if modifyActualPixels: 

464 # check that ACTUAL_PIXELS to PIXELS has been modified as expected 

465 desiredActualPixelsToPixels = pixelTransform.then(actualPixelsToPixels) 

466 self.assertPairListsAlmostEqual(modifiedActualPixelsToPixels.applyForward(pixPointList), 

467 desiredActualPixelsToPixels.applyForward(pixPointList)) 

468 

469 # check that PIXELS to IWC is unchanged 

470 originalPixelsToIwc = TransformPoint2ToPoint2(originalFrameDict.getMapping("PIXELS", "IWC")) 

471 self.assertPairListsAlmostEqual(modifiedPixelsToIwc.applyForward(pixPointList), 

472 originalPixelsToIwc.applyForward(pixPointList)) 

473 

474 else: 

475 # check that ACTUAL_PIXELS to PIXELS is unchanged 

476 self.assertPairListsAlmostEqual(actualPixelsToPixels.applyForward(pixPointList), 

477 actualPixelsToPixels.applyForward(pixPointList)) 

478 

479 # check that PIXELS to IWC has been modified as expected 

480 desiredPixelsToIwc = TransformPoint2ToPoint2( 

481 pixelTransform.getMapping().then(originalFrameDict.getMapping("PIXELS", "IWC"))) 

482 self.assertPairListsAlmostEqual(modifiedPixelsToIwc.applyForward(pixPointList), 

483 desiredPixelsToIwc.applyForward(pixPointList)) 

484 

485 self.checkNonFitsWcs(modifiedWcs) 

486 

487 def testMakeSkyWcsFromPixelsToFieldAngle(self): 

488 """Test makeSkyWcs from a pixelsToFieldAngle transform 

489 """ 

490 pixelSizeMm = 25e-3 

491 # place the detector in several positions at several orientations 

492 # use fewer CRVAL and orientations to speed up the test 

493 for fpPosition, yaw, addOpticalDistortion, crval, pixelOrientation, \ 

494 flipX, projection in itertools.product( 

495 (lsst.geom.Point3D(0, 0, 0), lsst.geom.Point3D(-100, 500, 1.5)), 

496 (0*lsst.geom.degrees, 71*lsst.geom.degrees), (False, True), 

497 self.crvalList[0:2], self.orientationList[0:2], (False, True), ("TAN", "STG")): 

498 with self.subTest(fpPosition=fpPosition, yaw=yaw, addOpticalDistortion=addOpticalDistortion, 

499 crval=crval, orientation=pixelOrientation): 

500 pixelsToFocalPlane = cameraGeom.Orientation( 

501 fpPosition=fpPosition, 

502 yaw=yaw, 

503 ).makePixelFpTransform(lsst.geom.Extent2D(pixelSizeMm, pixelSizeMm)) 

504 # Compute crpix before adding optical distortion, 

505 # since it is not affected by such distortion 

506 crpix = pixelsToFocalPlane.applyInverse(lsst.geom.Point2D(0, 0)) 

507 radiansPerMm = self.scale.asRadians() / pixelSizeMm 

508 focalPlaneToFieldAngle = lsst.afw.geom.makeTransform( 

509 lsst.geom.AffineTransform(lsst.geom.LinearTransform.makeScaling(radiansPerMm))) 

510 pixelsToFieldAngle = pixelsToFocalPlane.then(focalPlaneToFieldAngle) 

511 

512 cdMatrix = makeCdMatrix(scale=self.scale, orientation=pixelOrientation, flipX=flipX) 

513 wcs1 = makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix, projection=projection) 

514 

515 if addOpticalDistortion: 

516 # Model optical distortion as a pixel transform, 

517 # so it can be added to the WCS created from crpix, 

518 # cdMatrix, etc. using makeModifiedWcs 

519 pixelTransform = makeRadialTransform([0.0, 1.0, 0.0, 0.0011]) 

520 pixelsToFieldAngle = pixelTransform.then(pixelsToFieldAngle) 

521 wcs1 = makeModifiedWcs(pixelTransform=pixelTransform, wcs=wcs1, modifyActualPixels=False) 

522 

523 # orientation is with respect to detector x, y 

524 # but this flavor of makeSkyWcs needs it with respect to focal plane x, y 

525 focalPlaneOrientation = pixelOrientation + (yaw if flipX else -yaw) 

526 wcs2 = makeSkyWcs(pixelsToFieldAngle=pixelsToFieldAngle, 

527 orientation=focalPlaneOrientation, 

528 flipX=flipX, 

529 boresight=crval, 

530 projection=projection) 

531 self.assertWcsAlmostEqualOverBBox(wcs1, wcs2, self.bbox) 

532 

533 @unittest.skipIf(sys.version_info[0] < 3, "astropy.wcs rejects the header on py2") 

534 def testAgainstAstropyWcs(self): 

535 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-1000, -1000), lsst.geom.Extent2D(2000, 2000)) 

536 for crval, orientation, flipX, projection in itertools.product(self.crvalList, 

537 self.orientationList, 

538 (False, True), 

539 ("TAN", "STG", "CEA", "AIT")): 

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

541 metadata = makeSimpleWcsMetadata(crpix=self.crpix, crval=crval, cdMatrix=cdMatrix, 

542 projection=projection) 

543 header = makeLimitedFitsHeader(metadata) 

544 astropyWcs = astropy.wcs.WCS(header) 

545 skyWcs = makeSkyWcs(crpix=self.crpix, crval=crval, cdMatrix=cdMatrix, projection=projection) 

546 # Most projections only seem to agree to within 1e-4 in the round trip test 

547 self.assertSkyWcsAstropyWcsAlmostEqual(skyWcs=skyWcs, astropyWcs=astropyWcs, bbox=bbox) 

548 

549 def testPixelToSkyArray(self): 

550 """Test the numpy-array version of pixelToSky 

551 """ 

552 cdMatrix = makeCdMatrix(scale=self.scale) 

553 wcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

554 

555 xPoints = np.array([0.0, 1000.0, 0.0, -1111.0]) 

556 yPoints = np.array([0.0, 0.0, 2000.0, -2222.0]) 

557 

558 pixPointList = [lsst.geom.Point2D(x, y) for x, y in zip(xPoints, yPoints)] 

559 

560 spherePoints = wcs.pixelToSky(pixPointList) 

561 

562 ra, dec = wcs.pixelToSkyArray(xPoints, yPoints, degrees=False) 

563 for r, d, spherePoint in zip(ra, dec, spherePoints): 

564 self.assertAlmostEqual(r, spherePoint.getRa().asRadians()) 

565 self.assertAlmostEqual(d, spherePoint.getDec().asRadians()) 

566 

567 ra, dec = wcs.pixelToSkyArray(xPoints, yPoints, degrees=True) 

568 for r, d, spherePoint in zip(ra, dec, spherePoints): 

569 self.assertAlmostEqual(r, spherePoint.getRa().asDegrees()) 

570 self.assertAlmostEqual(d, spherePoint.getDec().asDegrees()) 

571 

572 def testSkyToPixelArray(self): 

573 """Test the numpy-array version of skyToPixel 

574 """ 

575 cdMatrix = makeCdMatrix(scale=self.scale) 

576 wcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

577 

578 raPoints = np.array([3.92646679e-02, 3.59646622e+02, 

579 3.96489283e-02, 4.70419353e-01]) 

580 decPoints = np.array([44.9722155, 44.97167735, 

581 45.52775599, 44.3540619]) 

582 

583 spherePointList = [lsst.geom.SpherePoint(ra*lsst.geom.degrees, 

584 dec*lsst.geom.degrees) 

585 for ra, dec in zip(raPoints, decPoints)] 

586 

587 pixPoints = wcs.skyToPixel(spherePointList) 

588 

589 x, y = wcs.skyToPixelArray(np.deg2rad(raPoints), np.deg2rad(decPoints)) 

590 for x0, y0, pixPoint in zip(x, y, pixPoints): 

591 self.assertAlmostEqual(x0, pixPoint.getX()) 

592 self.assertAlmostEqual(y0, pixPoint.getY()) 

593 

594 x, y = wcs.skyToPixelArray(raPoints, decPoints, degrees=True) 

595 for x0, y0, pixPoint in zip(x, y, pixPoints): 

596 self.assertAlmostEqual(x0, pixPoint.getX()) 

597 self.assertAlmostEqual(y0, pixPoint.getY()) 

598 

599 def testStr(self): 

600 """Test that we can get something coherent when printing a SkyWcs. 

601 """ 

602 cdMatrix = makeCdMatrix(scale=self.scale) 

603 skyWcs = makeSkyWcs(crpix=self.crpix, crval=self.crvalList[0], cdMatrix=cdMatrix) 

604 self.assertIn(f"Sky Origin: {self.crvalList[0]}", str(skyWcs)) 

605 self.assertIn(f"Pixel Origin: {self.crpix}", str(skyWcs)) 

606 self.assertIn("Pixel Scale: ", str(skyWcs)) 

607 

608 

609class MetadataWcsTestCase(SkyWcsBaseTestCase): 

610 """Test metadata constructor of SkyWcs 

611 """ 

612 

613 def setUp(self): 

614 metadata = PropertyList() 

615 for name, value in ( 

616 ("RADESYS", "ICRS"), 

617 ("EQUINOX", 2000.), 

618 ("CRVAL1", 215.604025685476), 

619 ("CRVAL2", 53.1595451514076), 

620 ("CRPIX1", 1109.99981456774), 

621 ("CRPIX2", 560.018167811613), 

622 ("CTYPE1", "RA---TAN"), 

623 ("CTYPE2", "DEC--TAN"), 

624 ("CUNIT1", "deg"), 

625 ("CUNIT2", "deg"), 

626 ("CD1_1", 5.10808596133527E-05), 

627 ("CD1_2", 1.85579539217196E-07), 

628 ("CD2_2", -5.10281493481982E-05), 

629 ("CD2_1", -1.85579539217196E-07), 

630 ): 

631 metadata.set(name, value) 

632 self.metadata = metadata 

633 

634 def tearDown(self): 

635 del self.metadata 

636 

637 def checkWcs(self, skyWcs): 

638 pixelOrigin = skyWcs.getPixelOrigin() 

639 skyOrigin = skyWcs.getSkyOrigin() 

640 for i in range(2): 

641 # subtract 1 from FITS CRPIX to get LSST convention 

642 self.assertAlmostEqual(pixelOrigin[i], self.metadata.getScalar(f"CRPIX{i+1}") - 1) 

643 self.assertAnglesAlmostEqual(skyOrigin[i], 

644 self.metadata.getScalar(f"CRVAL{i+1}")*lsst.geom.degrees) 

645 cdMatrix = skyWcs.getCdMatrix() 

646 for i, j in itertools.product(range(2), range(2)): 

647 self.assertAlmostEqual(cdMatrix[i, j], self.metadata.getScalar(f"CD{i+1}_{j+1}")) 

648 

649 self.assertTrue(skyWcs.isFits) 

650 

651 skyWcsCopy = SkyWcs.readString(skyWcs.writeString()) 

652 self.assertTrue(skyWcsCopy.isFits) 

653 self.checkMakeFlippedWcs(skyWcs) 

654 

655 @unittest.skipIf(sys.version_info[0] < 3, "astropy.wcs rejects the header on py2") 

656 def testAgainstAstropyWcs(self): 

657 skyWcs = makeSkyWcs(self.metadata, strip=False) 

658 header = makeLimitedFitsHeader(self.metadata) 

659 astropyWcs = astropy.wcs.WCS(header) 

660 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-1000, -1000), lsst.geom.Extent2D(3000, 3000)) 

661 self.assertSkyWcsAstropyWcsAlmostEqual(skyWcs=skyWcs, astropyWcs=astropyWcs, bbox=bbox) 

662 

663 def testLinearizeMethods(self): 

664 skyWcs = makeSkyWcs(self.metadata) 

665 # use a sky position near, but not at, the WCS origin 

666 sky00 = skyWcs.getSkyOrigin().offset(45 * lsst.geom.degrees, 1.2 * lsst.geom.degrees) 

667 pix00 = skyWcs.skyToPixel(sky00) 

668 for skyUnit in (lsst.geom.degrees, lsst.geom.radians): 

669 linPixToSky1 = skyWcs.linearizePixelToSky(sky00, skyUnit) # should match inverse of linSkyToPix1 

670 linPixToSky2 = skyWcs.linearizePixelToSky(pix00, skyUnit) # should match inverse of linSkyToPix1 

671 linSkyToPix1 = skyWcs.linearizeSkyToPixel(sky00, skyUnit) 

672 linSkyToPix2 = skyWcs.linearizeSkyToPixel(pix00, skyUnit) # should match linSkyToPix1 

673 

674 for pixel in (pix00, pix00 + lsst.geom.Extent2D(1000, -1230)): 

675 linSky = linPixToSky1(pixel) 

676 self.assertPairsAlmostEqual(linPixToSky2(pixel), linSky) 

677 self.assertPairsAlmostEqual(linSkyToPix1(linSky), pixel) 

678 self.assertPairsAlmostEqual(linSkyToPix2(linSky), pixel) 

679 

680 sky00Doubles = sky00.getPosition(skyUnit) 

681 pix00gApprox = linSkyToPix1(sky00Doubles) 

682 self.assertPairsAlmostEqual(pix00gApprox, pix00) 

683 self.assertAlmostEqual(pix00.getX(), pix00gApprox.getX()) 

684 self.assertAlmostEqual(pix00.getY(), pix00gApprox.getY()) 

685 pixelScale = skyWcs.getPixelScale(pix00) 

686 pixelArea = pixelScale.asAngularUnits(skyUnit)**2 

687 predictedPixelArea = 1 / linSkyToPix1.getLinear().computeDeterminant() 

688 self.assertAlmostEqual(pixelArea, predictedPixelArea) 

689 

690 def testBasics(self): 

691 skyWcs = makeSkyWcs(self.metadata, strip=False) 

692 self.assertEqual(len(self.metadata.names(False)), 14) 

693 self.checkWcs(skyWcs) 

694 makeSkyWcs(self.metadata, strip=True) 

695 self.assertEqual(len(self.metadata.names(False)), 0) 

696 

697 def testNormalizationFk5(self): 

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

699 """ 

700 equinox = 1975.0 

701 metadata = self.metadata 

702 

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

704 metadata.set("EQUINOX", equinox) 

705 crpix = lsst.geom.Point2D(metadata.getScalar("CRPIX1") - 1, metadata.getScalar("CRPIX2") - 1) 

706 # record the original CRVAL before reading and stripping metadata 

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

708 

709 # create the wcs and retrieve crval 

710 skyWcs = makeSkyWcs(metadata) 

711 crval = skyWcs.getSkyOrigin() 

712 

713 # compare to computed crval 

714 computedCrval = skyWcs.pixelToSky(crpix) 

715 self.assertSpherePointsAlmostEqual(crval, computedCrval) 

716 

717 # get predicted crval by converting with astropy 

718 crvalFk5 = astropy.coordinates.SkyCoord(crvalFk5Deg[0], crvalFk5Deg[1], frame="fk5", 

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

720 predictedCrvalIcrs = crvalFk5.icrs 

721 predictedCrval = lsst.geom.SpherePoint(predictedCrvalIcrs.ra.radian, predictedCrvalIcrs.dec.radian, 

722 lsst.geom.radians) 

723 self.assertSpherePointsAlmostEqual(crval, predictedCrval, maxSep=0.002*lsst.geom.arcseconds) 

724 

725 def testNormalizationDecRa(self): 

726 """Test that a Dec, RA WCS is normalized to RA, Dec 

727 """ 

728 crpix = lsst.geom.Point2D(self.metadata.getScalar("CRPIX1") - 1, 

729 self.metadata.getScalar("CRPIX2") - 1) 

730 

731 # swap RA, Decaxes in metadata 

732 crvalIn = lsst.geom.SpherePoint(self.metadata.getScalar("CRVAL1"), 

733 self.metadata.getScalar("CRVAL2"), lsst.geom.degrees) 

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

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

736 self.metadata.set("CTYPE1", "DEC--TAN") 

737 self.metadata.set("CTYPE2", "RA---TAN") 

738 

739 # create the wcs 

740 skyWcs = makeSkyWcs(self.metadata) 

741 

742 # compare pixel origin to input crval 

743 crval = skyWcs.getSkyOrigin() 

744 self.assertSpherePointsAlmostEqual(crval, crvalIn) 

745 

746 # compare to computed crval 

747 computedCrval = skyWcs.pixelToSky(crpix) 

748 self.assertSpherePointsAlmostEqual(crval, computedCrval) 

749 

750 def testReadDESHeader(self): 

751 """Verify that we can read a DES header""" 

752 self.metadata.set("RADESYS", "ICRS ") # note trailing white space 

753 self.metadata.set("CTYPE1", "RA---TPV") 

754 self.metadata.set("CTYPE2", "DEC--TPV") 

755 

756 skyWcs = makeSkyWcs(self.metadata, strip=False) 

757 self.checkWcs(skyWcs) 

758 

759 def testCD_PC(self): 

760 """Test that we can read a FITS file with both CD and PC keys (like early Suprimecam files)""" 

761 md = PropertyList() 

762 for k, v in ( 

763 ("EQUINOX", 2000.0), 

764 ("RADESYS", "ICRS"), 

765 ("CRPIX1", 5353.0), 

766 ("CRPIX2", -35.0), 

767 ("CD1_1", 0.0), 

768 ("CD1_2", -5.611E-05), 

769 ("CD2_1", -5.611E-05), 

770 ("CD2_2", -0.0), 

771 ("CRVAL1", 4.5789875), 

772 ("CRVAL2", 16.30004444), 

773 ("CUNIT1", "deg"), 

774 ("CUNIT2", "deg"), 

775 ("CTYPE1", "RA---TAN"), 

776 ("CTYPE2", "DEC--TAN"), 

777 ("CDELT1", -5.611E-05), 

778 ("CDELT2", 5.611E-05), 

779 ): 

780 md.set(k, v) 

781 

782 wcs = makeSkyWcs(md, strip=False) 

783 

784 pixPos = lsst.geom.Point2D(1000, 2000) 

785 pred_skyPos = lsst.geom.SpherePoint(4.459815023498577, 16.544199850984768, lsst.geom.degrees) 

786 

787 skyPos = wcs.pixelToSky(pixPos) 

788 self.assertSpherePointsAlmostEqual(skyPos, pred_skyPos) 

789 

790 for badPC in (False, True): 

791 for k, v in ( 

792 ("PC001001", 0.0), 

793 ("PC001002", -1.0 if badPC else 1.0), 

794 ("PC002001", 1.0 if badPC else -1.0), 

795 ("PC002002", 0.0), 

796 ): 

797 md.set(k, v) 

798 

799 # Check Greisen and Calabretta A&A 395 1061 (2002), Eq. 3 

800 if not badPC: 

801 for i in (1, 2,): 

802 for j in (1, 2,): 

803 self.assertEqual(md.getScalar(f"CD{i}_{j}"), 

804 md.getScalar(f"CDELT{i}")*md.getScalar(f"PC00{i}00{j}")) 

805 

806 wcs2 = makeSkyWcs(md, strip=False) 

807 skyPos2 = wcs2.pixelToSky(pixPos) 

808 self.assertSpherePointsAlmostEqual(skyPos2, pred_skyPos) 

809 

810 def testNoEpoch(self): 

811 """Ensure we're not writing epoch headers (DATE-OBS, MJD-OBS)""" 

812 self.metadata.set("EQUINOX", 2000.0) # Triggers AST writing DATE-OBS, MJD-OBS 

813 skyWcs = makeSkyWcs(self.metadata) 

814 header = skyWcs.getFitsMetadata() 

815 self.assertFalse(header.exists("DATE-OBS")) 

816 self.assertFalse(header.exists("MJD-OBS")) 

817 

818 def testCdMatrix(self): 

819 """Ensure we're writing CD matrix elements even if they're zero""" 

820 self.metadata.remove("CD1_2") 

821 self.metadata.remove("CD2_1") 

822 skyWcs = makeSkyWcs(self.metadata, strip=False) 

823 header = skyWcs.getFitsMetadata() 

824 for keyword in ("CD1_1", "CD2_2"): 

825 # There's some mild rounding going on 

826 self.assertFloatsAlmostEqual(header.get(keyword), self.metadata.get(keyword), atol=1.0e-16) 

827 for keyword in ("CD1_2", "CD2_1"): 

828 self.assertTrue(header.exists(keyword)) 

829 self.assertEqual(header.get(keyword), 0.0) 

830 

831 

832class TestTanSipTestCase(SkyWcsBaseTestCase): 

833 

834 def setUp(self): 

835 metadata = PropertyList() 

836 # the following was fit using CreateWcsWithSip from meas_astrom 

837 # and is valid over this bbox: (minimum=(0, 0), maximum=(3030, 3030)) 

838 # This same metadata was used to create testdata/oldTanSipwWs.fits 

839 for name, value in ( 

840 ("RADESYS", "ICRS"), 

841 ("CTYPE1", "RA---TAN-SIP"), 

842 ("CTYPE2", "DEC--TAN-SIP"), 

843 ("CRPIX1", 1531.1824767147), 

844 ("CRPIX2", 1531.1824767147), 

845 ("CRVAL1", 43.035511801383), 

846 ("CRVAL2", 44.305697682784), 

847 ("CUNIT1", "deg"), 

848 ("CUNIT2", "deg"), 

849 ("CD1_1", 0.00027493991598151), 

850 ("CD1_2", -3.2758487104158e-06), 

851 ("CD2_1", 3.2301310675830e-06), 

852 ("CD2_2", 0.00027493937506632), 

853 ("A_ORDER", 5), 

854 ("A_0_2", -1.7769487466972e-09), 

855 ("A_0_3", 5.3745894718340e-13), 

856 ("A_0_4", -7.2921116596880e-17), 

857 ("A_0_5", 8.6947236956136e-21), 

858 ("A_1_1", 5.4246387438098e-08), 

859 ("A_1_2", -1.5689083084641e-12), 

860 ("A_1_3", 1.2424130500997e-16), 

861 ("A_1_4", 3.9982572658006e-20), 

862 ("A_2_0", 4.9268299826160e-08), 

863 ("A_2_1", 1.6365657558495e-12), 

864 ("A_2_2", 1.1976983061953e-16), 

865 ("A_2_3", -1.7262037266467e-19), 

866 ("A_3_0", -5.9235031179999e-13), 

867 ("A_3_1", -3.4444326387310e-16), 

868 ("A_3_2", 1.4377441160800e-19), 

869 ("A_4_0", 1.8736407845095e-16), 

870 ("A_4_1", 2.9213314172884e-20), 

871 ("A_5_0", -5.3601346091084e-20), 

872 ("B_ORDER", 5), 

873 ("B_0_2", 4.9268299822979e-08), 

874 ("B_0_3", -5.9235032026906e-13), 

875 ("B_0_4", 1.8736407776035e-16), 

876 ("B_0_5", -5.3601341373220e-20), 

877 ("B_1_1", 5.4246387435453e-08), 

878 ("B_1_2", 1.6365657531115e-12), 

879 ("B_1_3", -3.4444326228808e-16), 

880 ("B_1_4", 2.9213312399941e-20), 

881 ("B_2_0", -1.7769487494962e-09), 

882 ("B_2_1", -1.5689082999319e-12), 

883 ("B_2_2", 1.1976983393279e-16), 

884 ("B_2_3", 1.4377441169892e-19), 

885 ("B_3_0", 5.3745894237186e-13), 

886 ("B_3_1", 1.2424130479929e-16), 

887 ("B_3_2", -1.7262036838229e-19), 

888 ("B_4_0", -7.2921117326608e-17), 

889 ("B_4_1", 3.9982566975450e-20), 

890 ("B_5_0", 8.6947240592408e-21), 

891 ("AP_ORDER", 6), 

892 ("AP_0_0", -5.4343024221207e-11), 

893 ("AP_0_1", 5.5722265946666e-12), 

894 ("AP_0_2", 1.7769484042400e-09), 

895 ("AP_0_3", -5.3773609554820e-13), 

896 ("AP_0_4", 7.3035278852156e-17), 

897 ("AP_0_5", -8.7151153799062e-21), 

898 ("AP_0_6", 3.2535945427624e-27), 

899 ("AP_1_0", -3.8944805432871e-12), 

900 ("AP_1_1", -5.4246388067582e-08), 

901 ("AP_1_2", 1.5741716194971e-12), 

902 ("AP_1_3", -1.2447067748187e-16), 

903 ("AP_1_4", -3.9960260822306e-20), 

904 ("AP_1_5", 1.1297941471380e-26), 

905 ("AP_2_0", -4.9268299293185e-08), 

906 ("AP_2_1", -1.6256111849359e-12), 

907 ("AP_2_2", -1.1973373130440e-16), 

908 ("AP_2_3", 1.7266948205700e-19), 

909 ("AP_2_4", -3.7059606160753e-26), 

910 ("AP_3_0", 5.9710911995811e-13), 

911 ("AP_3_1", 3.4464427650041e-16), 

912 ("AP_3_2", -1.4381853884204e-19), 

913 ("AP_3_3", -7.6527426974322e-27), 

914 ("AP_4_0", -1.8748435698960e-16), 

915 ("AP_4_1", -2.9267280226373e-20), 

916 ("AP_4_2", 4.8004317051259e-26), 

917 ("AP_5_0", 5.3657330221120e-20), 

918 ("AP_5_1", -1.6904065766661e-27), 

919 ("AP_6_0", -1.9484495120493e-26), 

920 ("BP_ORDER", 6), 

921 ("BP_0_0", -5.4291220607725e-11), 

922 ("BP_0_1", -3.8944871307931e-12), 

923 ("BP_0_2", -4.9268299290361e-08), 

924 ("BP_0_3", 5.9710912831833e-13), 

925 ("BP_0_4", -1.8748435594265e-16), 

926 ("BP_0_5", 5.3657325543368e-20), 

927 ("BP_0_6", -1.9484577299247e-26), 

928 ("BP_1_0", 5.5722051513577e-12), 

929 ("BP_1_1", -5.4246388065000e-08), 

930 ("BP_1_2", -1.6256111821465e-12), 

931 ("BP_1_3", 3.4464427499767e-16), 

932 ("BP_1_4", -2.9267278448109e-20), 

933 ("BP_1_5", -1.6904244067295e-27), 

934 ("BP_2_0", 1.7769484069376e-09), 

935 ("BP_2_1", 1.5741716110182e-12), 

936 ("BP_2_2", -1.1973373446176e-16), 

937 ("BP_2_3", -1.4381853893526e-19), 

938 ("BP_2_4", 4.8004294492911e-26), 

939 ("BP_3_0", -5.3773609074713e-13), 

940 ("BP_3_1", -1.2447067726801e-16), 

941 ("BP_3_2", 1.7266947774875e-19), 

942 ("BP_3_3", -7.6527556667042e-27), 

943 ("BP_4_0", 7.3035279660505e-17), 

944 ("BP_4_1", -3.9960255158200e-20), 

945 ("BP_4_2", -3.7059659675039e-26), 

946 ("BP_5_0", -8.7151157361284e-21), 

947 ("BP_5_1", 1.1297944388060e-26), 

948 ("BP_6_0", 3.2535788867488e-27), 

949 ): 

950 metadata.set(name, value) 

951 self.metadata = metadata 

952 self.bbox = lsst.geom.Box2D(lsst.geom.Point2D(-1000, -1000), lsst.geom.Extent2D(3000, 3000)) 

953 

954 def testTanSipFromFrameDict(self): 

955 """Test making a TAN-SIP WCS from a FrameDict 

956 """ 

957 skyWcs = makeSkyWcs(self.metadata, strip=False) 

958 self.checkFrameDictConstructor(skyWcs, bbox=self.bbox) 

959 

960 def testFitsMetadata(self): 

961 """Test that getFitsMetadata works for TAN-SIP 

962 """ 

963 skyWcs = makeSkyWcs(self.metadata, strip=False) 

964 self.assertTrue(skyWcs.isFits) 

965 fitsMetadata = skyWcs.getFitsMetadata(precise=True) 

966 skyWcsCopy = makeSkyWcs(fitsMetadata) 

967 self.assertWcsAlmostEqualOverBBox(skyWcs, skyWcsCopy, self.bbox) 

968 self.checkPersistence(skyWcs, bbox=self.bbox) 

969 

970 def testGetIntermediateWorldCoordsToSky(self): 

971 """Test getIntermediateWorldCoordsToSky and getPixelToIntermediateWorldCoords 

972 """ 

973 crpix = lsst.geom.Extent2D(self.metadata.getScalar("CRPIX1") - 1, 

974 self.metadata.getScalar("CRPIX2") - 1) 

975 skyWcs = makeSkyWcs(self.metadata, strip=False) 

976 for simplify in (False, True): 

977 pixelToIwc = getPixelToIntermediateWorldCoords(skyWcs, simplify) 

978 iwcToSky = getIntermediateWorldCoordsToSky(skyWcs, simplify) 

979 self.assertTrue(isinstance(pixelToIwc, TransformPoint2ToPoint2)) 

980 self.assertTrue(isinstance(iwcToSky, TransformPoint2ToSpherePoint)) 

981 if simplify: 

982 self.assertTrue(pixelToIwc.getMapping().isSimple) 

983 self.assertTrue(iwcToSky.getMapping().isSimple) 

984 # else the mapping may have already been simplified inside the WCS, 

985 # so don't assert isSimple is false 

986 

987 # check that the chained transforms produce the same results as the WCS 

988 # in the forward and inverse direction 

989 pixPosList = [] 

990 for dx in (0, 1000): 

991 for dy in (0, 1000): 

992 pixPosList.append(lsst.geom.Point2D(dx, dy) + crpix) 

993 iwcPosList = pixelToIwc.applyForward(pixPosList) 

994 skyPosList = iwcToSky.applyForward(iwcPosList) 

995 self.assertSpherePointListsAlmostEqual(skyPosList, skyWcs.pixelToSky(pixPosList)) 

996 self.assertPairListsAlmostEqual(pixelToIwc.applyInverse(iwcToSky.applyInverse(skyPosList)), 

997 skyWcs.skyToPixel(skyPosList)) 

998 

999 self.assertPairListsAlmostEqual(iwcPosList, iwcToSky.applyInverse(skyPosList)) 

1000 self.assertPairListsAlmostEqual(pixPosList, pixelToIwc.applyInverse(iwcPosList)) 

1001 

1002 # compare extracted pixelToIwc to a version of pixelToIwc computed directly from the metadata 

1003 ourPixelToIwc = makeSipPixelToIwc(self.metadata) 

1004 self.assertPairListsAlmostEqual(pixelToIwc.applyForward(pixPosList), 

1005 ourPixelToIwc.applyForward(pixPosList)) 

1006 

1007 # compare extracted iwcToPixel to a version of iwcToPixel computed directly from the metadata 

1008 ourIwcToPixel = makeSipIwcToPixel(self.metadata) 

1009 self.assertPairListsAlmostEqual(pixelToIwc.applyInverse(iwcPosList), 

1010 ourIwcToPixel.applyForward(iwcPosList)) 

1011 

1012 @unittest.skipIf(sys.version_info[0] < 3, "astropy.wcs rejects the header on py2") 

1013 def testAgainstAstropyWcs(self): 

1014 skyWcs = makeSkyWcs(self.metadata, strip=False) 

1015 header = makeLimitedFitsHeader(self.metadata) 

1016 astropyWcs = astropy.wcs.WCS(header) 

1017 self.assertSkyWcsAstropyWcsAlmostEqual(skyWcs=skyWcs, astropyWcs=astropyWcs, bbox=self.bbox) 

1018 

1019 def testMakeTanSipWcs(self): 

1020 referenceWcs = makeSkyWcs(self.metadata, strip=False) 

1021 

1022 crpix = lsst.geom.Point2D(self.metadata.getScalar("CRPIX1") - 1, 

1023 self.metadata.getScalar("CRPIX2") - 1) 

1024 crval = lsst.geom.SpherePoint(self.metadata.getScalar("CRVAL1"), 

1025 self.metadata.getScalar("CRVAL2"), lsst.geom.degrees) 

1026 cdMatrix = getCdMatrixFromMetadata(self.metadata) 

1027 sipA = getSipMatrixFromMetadata(self.metadata, "A") 

1028 sipB = getSipMatrixFromMetadata(self.metadata, "B") 

1029 sipAp = getSipMatrixFromMetadata(self.metadata, "AP") 

1030 sipBp = getSipMatrixFromMetadata(self.metadata, "BP") 

1031 skyWcs1 = makeTanSipWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix, sipA=sipA, sipB=sipB) 

1032 skyWcs2 = makeTanSipWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix, sipA=sipA, sipB=sipB, 

1033 sipAp=sipAp, sipBp=sipBp) 

1034 

1035 self.assertWcsAlmostEqualOverBBox(referenceWcs, skyWcs1, self.bbox) 

1036 self.assertWcsAlmostEqualOverBBox(referenceWcs, skyWcs2, self.bbox) 

1037 self.checkMakeFlippedWcs(skyWcs1) 

1038 self.checkMakeFlippedWcs(skyWcs2) 

1039 

1040 def testReadWriteFits(self): 

1041 wcsFromMetadata = makeSkyWcs(self.metadata) 

1042 with lsst.utils.tests.getTempFilePath(".fits") as filePath: 

1043 wcsFromMetadata.writeFits(filePath) 

1044 wcsFromFits = SkyWcs.readFits(filePath) 

1045 

1046 self.assertWcsAlmostEqualOverBBox(wcsFromFits, wcsFromMetadata, self.bbox, maxDiffPix=0, 

1047 maxDiffSky=0*lsst.geom.radians) 

1048 

1049 def testReadOldTanSipFits(self): 

1050 """Test reading a FITS file containing data for an lsst::afw::image::TanWcs 

1051 

1052 That file was made using the same metadata as this test 

1053 """ 

1054 dataDir = os.path.join(os.path.split(__file__)[0], "data") 

1055 filePath = os.path.join(dataDir, "oldTanSipWcs.fits") 

1056 wcsFromFits = SkyWcs.readFits(filePath) 

1057 

1058 wcsFromMetadata = makeSkyWcs(self.metadata) 

1059 

1060 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-1000, -1000), lsst.geom.Extent2D(3000, 3000)) 

1061 self.assertWcsAlmostEqualOverBBox(wcsFromFits, wcsFromMetadata, bbox) 

1062 

1063 def testReadOldTanFits(self): 

1064 """Test reading a FITS file containing data for an lsst::afw::image::TanWcs 

1065 

1066 That file was made using the same metadata follows 

1067 (like self.metadata without the distortion) 

1068 """ 

1069 tanMetadata = PropertyList() 

1070 # the following was fit using CreateWcsWithSip from meas_astrom 

1071 # and is valid over this bbox: (minimum=(0, 0), maximum=(3030, 3030)) 

1072 # This same metadata was used to create testdata/oldTanSipwWs.fits 

1073 for name, value in ( 

1074 ("RADESYS", "ICRS"), 

1075 ("CTYPE1", "RA---TAN"), 

1076 ("CTYPE2", "DEC--TAN"), 

1077 ("CRPIX1", 1531.1824767147), 

1078 ("CRPIX2", 1531.1824767147), 

1079 ("CRVAL1", 43.035511801383), 

1080 ("CRVAL2", 44.305697682784), 

1081 ("CUNIT1", "deg"), 

1082 ("CUNIT2", "deg"), 

1083 ("CD1_1", 0.00027493991598151), 

1084 ("CD1_2", -3.2758487104158e-06), 

1085 ("CD2_1", 3.2301310675830e-06), 

1086 ("CD2_2", 0.00027493937506632), 

1087 ): 

1088 tanMetadata.set(name, value) 

1089 

1090 dataDir = os.path.join(os.path.split(__file__)[0], "data") 

1091 filePath = os.path.join(dataDir, "oldTanWcs.fits") 

1092 wcsFromFits = SkyWcs.readFits(filePath) 

1093 

1094 wcsFromMetadata = makeSkyWcs(tanMetadata) 

1095 

1096 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-1000, -1000), lsst.geom.Extent2D(3000, 3000)) 

1097 self.assertWcsAlmostEqualOverBBox(wcsFromFits, wcsFromMetadata, bbox) 

1098 

1099 

1100class WcsPairTransformTestCase(SkyWcsBaseTestCase): 

1101 """Test functionality of makeWcsPairTransform. 

1102 """ 

1103 def setUp(self): 

1104 SkyWcsBaseTestCase.setUp(self) 

1105 crpix = lsst.geom.Point2D(100, 100) 

1106 crvalList = [ 

1107 lsst.geom.SpherePoint(0, 45, lsst.geom.degrees), 

1108 lsst.geom.SpherePoint(0.00001, 45, lsst.geom.degrees), 

1109 lsst.geom.SpherePoint(359.99999, 45, lsst.geom.degrees), 

1110 lsst.geom.SpherePoint(30, 89.99999, lsst.geom.degrees), 

1111 ] 

1112 orientationList = [ 

1113 0 * lsst.geom.degrees, 

1114 0.00001 * lsst.geom.degrees, 

1115 -0.00001 * lsst.geom.degrees, 

1116 -45 * lsst.geom.degrees, 

1117 90 * lsst.geom.degrees, 

1118 ] 

1119 scale = 1.0 * lsst.geom.arcseconds 

1120 

1121 self.wcsList = [] 

1122 for crval in crvalList: 

1123 for orientation in orientationList: 

1124 cd = makeCdMatrix(scale=scale, orientation=orientation) 

1125 self.wcsList.append(makeSkyWcs( 

1126 crpix=crpix, 

1127 crval=crval, 

1128 cdMatrix=cd)) 

1129 self.pixelPoints = [lsst.geom.Point2D(x, y) for x, y in 

1130 itertools.product((0.0, -2.0, 42.5, 1042.3), 

1131 (27.6, -0.1, 0.0, 196.0))] 

1132 

1133 def testGenericWcs(self): 

1134 """Test that input and output points represent the same sky position. 

1135 

1136 Would prefer a black-box test, but don't have the numbers for it. 

1137 """ 

1138 inPoints = self.pixelPoints 

1139 for wcs1 in self.wcsList: 

1140 for wcs2 in self.wcsList: 

1141 transform = makeWcsPairTransform(wcs1, wcs2) 

1142 outPoints = transform.applyForward(inPoints) 

1143 inPointsRoundTrip = transform.applyInverse(outPoints) 

1144 self.assertPairListsAlmostEqual(inPoints, inPointsRoundTrip) 

1145 self.assertSpherePointListsAlmostEqual(wcs1.pixelToSky(inPoints), 

1146 wcs2.pixelToSky(outPoints)) 

1147 

1148 def testSameWcs(self): 

1149 """Confirm that pairing two identical Wcs gives an identity transform. 

1150 """ 

1151 for wcs in self.wcsList: 

1152 transform = makeWcsPairTransform(wcs, wcs) 

1153 # check that the transform has been simplified 

1154 self.assertTrue(transform.getMapping().isSimple) 

1155 # check the transform 

1156 outPoints1 = transform.applyForward(self.pixelPoints) 

1157 outPoints2 = transform.applyInverse(outPoints1) 

1158 self.assertPairListsAlmostEqual(self.pixelPoints, outPoints1) 

1159 self.assertPairListsAlmostEqual(outPoints1, outPoints2) 

1160 

1161 

1162class TestMemory(lsst.utils.tests.MemoryTestCase): 

1163 pass 

1164 

1165 

1166def setup_module(module): 

1167 lsst.utils.tests.init() 

1168 

1169 

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

1171 lsst.utils.tests.init() 

1172 unittest.main()