Coverage for tests / test_skyWcs.py: 12%

566 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 01:18 -0700

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 

13from lsst.daf.base import PropertyList 

14import lsst.geom 

15import lsst.afw.cameraGeom as cameraGeom 

16from lsst.afw.geom import ( 

17 TransformPoint2ToPoint2, TransformPoint2ToSpherePoint, makeRadialTransform, 

18 SkyWcs, makeSkyWcs, makeCdMatrix, makeWcsPairTransform, 

19 makeFlippedWcs, makeModifiedWcs, makeTanSipWcs, 

20 getIntermediateWorldCoordsToSky, getPixelToIntermediateWorldCoords, 

21 stripWcsMetadata 

22) 

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

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

25from lsst.afw.fits import makeLimitedFitsHeader 

26from lsst.afw.image import ExposureF 

27 

28 

29def addActualPixelsFrame(skyWcs, actualPixelsToPixels): 

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

31 

32 Parameters 

33 ---------- 

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

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

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

37 The transform from ACTUAL_PIXELS to PIXELS 

38 """ 

39 actualPixelsToPixelsMap = actualPixelsToPixels.getMapping() 

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

41 frameDict = skyWcs.getFrameDict() 

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

43 frameDict.setBase("ACTUAL_PIXELS") 

44 frameDict.setCurrent("SKY") 

45 return SkyWcs(frameDict) 

46 

47 

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

49 def checkPersistence(self, skyWcs, bbox): 

50 """Check persistence of a SkyWcs 

51 """ 

52 className = "SkyWcs" 

53 

54 # check writeString and readString 

55 skyWcsStr = skyWcs.writeString() 

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

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

58 self.assertEqual(serialClassName, className) 

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

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

61 skyWcs.readString(badStr1) 

62 badClassName = "x" + serialClassName 

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

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

65 skyWcs.readString(badStr2) 

66 skyWcsFromStr1 = skyWcs.readString(skyWcsStr) 

67 self.assertEqual(skyWcs, skyWcsFromStr1) 

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

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

70 

71 pixelPoints = [ 

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

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

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

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

76 ] 

77 skyPoints = skyWcs.pixelToSky(pixelPoints) 

78 pixelPoints2 = skyWcs.skyToPixel(skyPoints) 

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

80 

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

82 exposure = ExposureF(100, 100, skyWcs) 

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

84 exposure.writeFits(outFile) 

85 exposureRoundTrip = ExposureF(outFile) 

86 wcsFromExposure = exposureRoundTrip.getWcs() 

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

88 maxDiffSky=0*lsst.geom.radians) 

89 

90 def checkFrameDictConstructor(self, skyWcs, bbox): 

91 """Check that the FrameDict constructor works 

92 """ 

93 frameDict = skyWcs.getFrameDict() 

94 wcsFromFrameDict = SkyWcs(frameDict) 

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

96 maxDiffSky=0*lsst.geom.radians) 

97 

98 self.checkPersistence(wcsFromFrameDict, bbox) 

99 

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

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

102 badFrameDict = skyWcs.getFrameDict() 

103 badFrameDict.removeFrame(domain) 

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

105 SkyWcs(badFrameDict) 

106 

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

108 """Check makeFlippedWcs on the provided WCS 

109 """ 

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

111 # and does not include zero in the other axis 

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

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

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

115 # dict of (isRight, isTop): position 

116 minPos = bbox.getMin() 

117 maxPos = bbox.getMax() 

118 center = bbox.getCenter() 

119 cornerDict = { 

120 (False, False): minPos, 

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

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

123 (True, True): maxPos, 

124 } 

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

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

127 # the center is unchanged 

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

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

130 

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

132 origPos = cornerDict[(isR, isT)] 

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

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

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

136 

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

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

139 checkRoundTrip=True): 

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

141 """ 

142 bbox = lsst.geom.Box2D(bbox) 

143 center = bbox.getCenter() 

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

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

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

147 

148 # pixelToSky 

149 skyPosList = skyWcs.pixelToSky(pixPosList) 

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

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

152 

153 if not checkRoundTrip: 

154 return 

155 

156 # astropy round trip 

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

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

159 

160 # SkyWcs round trip 

161 pixPosListRoundTrip = skyWcs.skyToPixel(skyPosList) 

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

163 

164 # skyToPixel astropy vs SkyWcs 

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

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

167 

168 def astropyPixelsToSky(self, astropyWcs, pixPosList): 

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

170 

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

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

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

174 

175 Converts the output to ICRS 

176 """ 

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

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

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

180 yp=yarr, 

181 wcs=astropyWcs, 

182 origin=0, 

183 mode="all") 

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

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

186 

187 def astropySkyToPixels(self, astropyWcs, skyPosList): 

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

189 

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

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

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

193 

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

195 """ 

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

197 c[1].asDegrees(), 

198 frame="icrs", 

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

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

201 wcs=astropyWcs, 

202 origin=0, 

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

204 # float is needed to avoid truncation to int 

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

206 

207 

208class SimpleSkyWcsTestCase(SkyWcsBaseTestCase): 

209 """Test the simple FITS version of makeSkyWcs 

210 """ 

211 

212 def setUp(self): 

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

214 self.crvalList = [ 

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

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

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

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

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

220 ] 

221 self.orientationList = [ 

222 0 * lsst.geom.degrees, 

223 0.00001 * lsst.geom.degrees, 

224 -0.00001 * lsst.geom.degrees, 

225 -45 * lsst.geom.degrees, 

226 90 * lsst.geom.degrees, 

227 ] 

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

229 self.tinyPixels = 1.0e-10 

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

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

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

233 

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

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

236 

237 Parameters 

238 ---------- 

239 crval : `lsst.geom.SpherePoint` 

240 Desired reference sky position. 

241 Must not be at either pole. 

242 orientation : `lsst.geom.Angle` 

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

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

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

246 flipX : `bool` 

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

248 

249 Returns 

250 ------- 

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

252 The generated pure TAN SkyWcs 

253 """ 

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

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

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

257 self.checkMakeFlippedWcs(wcs) 

258 

259 self.assertTrue(wcs.isFits) 

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

261 

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

263 

264 pixelList = [ 

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

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

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

268 ] 

269 skyList = wcs.pixelToSky(pixelList) 

270 

271 # check pixels to sky 

272 predSkyList = [ 

273 crval, 

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

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

276 ] 

277 self.assertSpherePointListsAlmostEqual(predSkyList, skyList) 

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

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

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

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

282 

283 # check sky to pixels 

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

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

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

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

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

289 

290 # check CRVAL round trip 

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

292 maxSep=self.tinyAngle) 

293 

294 crpix = wcs.getPixelOrigin() 

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

296 

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

298 

299 pixelScale = wcs.getPixelScale() 

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

301 

302 pixelScale = wcs.getPixelScale(self.crpix) 

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

304 

305 # check that getFitsMetadata can operate at high precision 

306 # and has axis order RA, Dec 

307 fitsMetadata = wcs.getFitsMetadata(True) 

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

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

310 

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

312 # The resulting sky origin should not change 

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

314 shiftedWcs = wcs.copyAtShiftedPixelOrigin(offset) 

315 self.assertTrue(shiftedWcs.isFits) 

316 predShiftedPixelOrigin = self.crpix + offset 

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

318 maxDiff=self.tinyPixels) 

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

320 

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

322 shiftedSkyList = shiftedWcs.pixelToSky(shiftedPixelList) 

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

324 

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

326 shiftedMetadata = shiftedWcs.getFitsMetadata(precise=True) 

327 shiftedWcsCopy = makeSkyWcs(shiftedMetadata) 

328 shiftedBBox = lsst.geom.Box2D(predShiftedPixelOrigin, 

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

330 self.assertWcsAlmostEqualOverBBox(shiftedWcs, shiftedWcsCopy, shiftedBBox) 

331 

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

333 self.assertTrue(wcsCopy.isFits) 

334 

335 return wcs 

336 

337 def checkNonFitsWcs(self, wcs): 

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

339 """ 

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

341 self.assertFalse(wcs.isFits) 

342 with self.assertRaises(RuntimeError): 

343 wcs.getFitsMetadata(True) 

344 with self.assertRaises(RuntimeError): 

345 wcs.getFitsMetadata(False) 

346 

347 # When a WCS is not valid FITS itself, we can explicitly install a FITS 

348 # approximation. The 'wcsApprox' below is just arbitrary, not really 

349 # an approximation in this test, but since SkyWcs shouldn't be 

350 # responsible for the accuracy of the approximation that's fine. 

351 wcsApprox = makeSkyWcs( 

352 lsst.geom.Point2D(5, 4), 

353 lsst.geom.SpherePoint(20.0, 30.0, lsst.geom.degrees), 

354 np.identity(2), 

355 ) 

356 wcsWithApproximation = wcs.copyWithFitsApproximation(wcsApprox) 

357 self.assertTrue(wcsWithApproximation.hasFitsApproximation()) 

358 self.assertEqual(wcsWithApproximation.getFitsApproximation(), wcsApprox) 

359 # When we save a SkyWcs with a FITS approximation it round-trips. 

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

361 wcsWithApproximation.writeFits(filePath) 

362 wcsFromFits = SkyWcs.readFits(filePath) 

363 self.assertTrue(wcsFromFits.hasFitsApproximation()) 

364 self.assertEqual(wcsFromFits.getFitsApproximation(), wcsApprox) 

365 # When we save an Exposure with a non-FITS SkyWcs that has a FITS 

366 # approximation, the approximation appears in the headers. 

367 exposure = ExposureF(lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(3, 2))) 

368 exposure.setWcs(wcsWithApproximation) 

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

370 exposure.writeFits(filePath) 

371 exposureFromFits = ExposureF(filePath) 

372 self.assertEqual(exposureFromFits.wcs, wcsWithApproximation) 

373 self.assertEqual(exposureFromFits.wcs.getFitsApproximation(), wcsApprox) 

374 with astropy.io.fits.open(filePath) as fits: 

375 self.assertEqual(fits[1].header["CRVAL1"], 20.0) 

376 self.assertEqual(fits[1].header["CRVAL2"], 30.0) 

377 

378 def testTanWcs(self): 

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

380 """ 

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

382 self.orientationList, 

383 (False, True)): 

384 self.checkTanWcs(crval=crval, 

385 orientation=orientation, 

386 flipX=flipX, 

387 ) 

388 

389 def testTanWcsFromFrameDict(self): 

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

391 """ 

392 cdMatrix = makeCdMatrix(scale=self.scale) 

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

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

395 

396 def testGetFrameDict(self): 

397 """Test that getFrameDict returns a deep copy 

398 """ 

399 cdMatrix = makeCdMatrix(scale=self.scale) 

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

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

402 frameDict = skyWcs.getFrameDict() 

403 frameDict.removeFrame(domain) 

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

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

406 

407 def testMakeModifiedWcsNoActualPixels(self): 

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

409 """ 

410 cdMatrix = makeCdMatrix(scale=self.scale) 

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

412 originalFrameDict = originalWcs.getFrameDict() 

413 

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

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

416 # the result of the insertion should be as follows 

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

418 

419 pixPointList = ( # arbitrary but reasonable 

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

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

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

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

424 ) 

425 for modifyActualPixels in (False, True): 

426 modifiedWcs = makeModifiedWcs(pixelTransform=pixelTransform, 

427 wcs=originalWcs, 

428 modifyActualPixels=modifyActualPixels) 

429 modifiedFrameDict = modifiedWcs.getFrameDict() 

430 skyList = modifiedWcs.pixelToSky(pixPointList) 

431 

432 # compare pixels to sky 

433 desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) 

434 self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) 

435 

436 # compare pixels to IWC 

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

438 desiredPixelsToIwc = TransformPoint2ToPoint2( 

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

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

441 desiredPixelsToIwc.applyForward(pixPointList)) 

442 

443 self.checkNonFitsWcs(modifiedWcs) 

444 

445 def testMakeModifiedWcsWithActualPixels(self): 

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

447 """ 

448 cdMatrix = makeCdMatrix(scale=self.scale) 

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

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

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

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

453 originalWcs = addActualPixelsFrame(baseWcs, actualPixelsToPixels) 

454 originalFrameDict = originalWcs.getFrameDict() 

455 

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

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

458 

459 pixPointList = ( # arbitrary but reasonable 

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

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

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

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

464 ) 

465 for modifyActualPixels in (True, False): 

466 modifiedWcs = makeModifiedWcs(pixelTransform=pixelTransform, 

467 wcs=originalWcs, 

468 modifyActualPixels=modifyActualPixels) 

469 modifiedFrameDict = modifiedWcs.getFrameDict() 

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

471 modifiedActualPixelsToPixels = \ 

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

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

474 

475 # compare pixels to sky 

476 skyList = modifiedWcs.pixelToSky(pixPointList) 

477 if modifyActualPixels: 

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

479 else: 

480 originalPixelsToSky = \ 

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

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

483 desiredSkyList = desiredPixelsToSky.applyForward(pixPointList) 

484 self.assertSpherePointListsAlmostEqual(skyList, desiredSkyList) 

485 

486 # compare ACTUAL_PIXELS to PIXELS and PIXELS to IWC 

487 if modifyActualPixels: 

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

489 desiredActualPixelsToPixels = pixelTransform.then(actualPixelsToPixels) 

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

491 desiredActualPixelsToPixels.applyForward(pixPointList)) 

492 

493 # check that PIXELS to IWC is unchanged 

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

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

496 originalPixelsToIwc.applyForward(pixPointList)) 

497 

498 else: 

499 # check that ACTUAL_PIXELS to PIXELS is unchanged 

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

501 actualPixelsToPixels.applyForward(pixPointList)) 

502 

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

504 desiredPixelsToIwc = TransformPoint2ToPoint2( 

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

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

507 desiredPixelsToIwc.applyForward(pixPointList)) 

508 

509 self.checkNonFitsWcs(modifiedWcs) 

510 

511 def testMakeSkyWcsFromPixelsToFieldAngle(self): 

512 """Test makeSkyWcs from a pixelsToFieldAngle transform 

513 """ 

514 pixelSizeMm = 25e-3 

515 # place the detector in several positions at several orientations 

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

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

518 flipX, projection in itertools.product( 

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

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

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

522 with self.subTest(fpPosition=repr(fpPosition), yaw=repr(yaw), 

523 addOpticalDistortion=repr(addOpticalDistortion), 

524 crval=repr(crval), orientation=repr(pixelOrientation)): 

525 pixelsToFocalPlane = cameraGeom.Orientation( 

526 fpPosition=fpPosition, 

527 yaw=yaw, 

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

529 # Compute crpix before adding optical distortion, 

530 # since it is not affected by such distortion 

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

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

533 focalPlaneToFieldAngle = lsst.afw.geom.makeTransform( 

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

535 pixelsToFieldAngle = pixelsToFocalPlane.then(focalPlaneToFieldAngle) 

536 

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

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

539 

540 if addOpticalDistortion: 

541 # Model optical distortion as a pixel transform, 

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

543 # cdMatrix, etc. using makeModifiedWcs 

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

545 pixelsToFieldAngle = pixelTransform.then(pixelsToFieldAngle) 

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

547 

548 # orientation is with respect to detector x, y 

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

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

551 wcs2 = makeSkyWcs(pixelsToFieldAngle=pixelsToFieldAngle, 

552 orientation=focalPlaneOrientation, 

553 flipX=flipX, 

554 boresight=crval, 

555 projection=projection) 

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

557 

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

559 def testAgainstAstropyWcs(self): 

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

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

562 self.orientationList, 

563 (False, True), 

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

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

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

567 projection=projection) 

568 header = makeLimitedFitsHeader(metadata) 

569 astropyWcs = astropy.wcs.WCS(header) 

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

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

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

573 

574 def testPixelToSkyArray(self): 

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

576 """ 

577 cdMatrix = makeCdMatrix(scale=self.scale) 

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

579 

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

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

582 

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

584 

585 spherePoints = wcs.pixelToSky(pixPointList) 

586 

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

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

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

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

591 

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

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

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

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

596 

597 def testSkyToPixelArray(self): 

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

599 """ 

600 cdMatrix = makeCdMatrix(scale=self.scale) 

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

602 

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

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

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

606 45.52775599, 44.3540619]) 

607 

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

609 dec*lsst.geom.degrees) 

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

611 

612 pixPoints = wcs.skyToPixel(spherePointList) 

613 

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

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

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

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

618 

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

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

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

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

623 

624 def testStr(self): 

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

626 """ 

627 cdMatrix = makeCdMatrix(scale=self.scale) 

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

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

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

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

632 

633 

634class MetadataWcsTestCase(SkyWcsBaseTestCase): 

635 """Test metadata constructor of SkyWcs 

636 """ 

637 

638 def setUp(self): 

639 metadata = PropertyList() 

640 for name, value in ( 

641 ("RADESYS", "ICRS"), 

642 ("EQUINOX", 2000.), 

643 ("CRVAL1", 215.604025685476), 

644 ("CRVAL2", 53.1595451514076), 

645 ("CRPIX1", 1109.99981456774), 

646 ("CRPIX2", 560.018167811613), 

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

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

649 ("CUNIT1", "deg"), 

650 ("CUNIT2", "deg"), 

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

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

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

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

655 ): 

656 metadata.set(name, value) 

657 self.metadata = metadata 

658 

659 def tearDown(self): 

660 del self.metadata 

661 

662 def checkWcs(self, skyWcs): 

663 pixelOrigin = skyWcs.getPixelOrigin() 

664 skyOrigin = skyWcs.getSkyOrigin() 

665 for i in range(2): 

666 # subtract 1 from FITS CRPIX to get LSST convention 

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

668 self.assertAnglesAlmostEqual(skyOrigin[i], 

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

670 cdMatrix = skyWcs.getCdMatrix() 

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

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

673 

674 self.assertTrue(skyWcs.isFits) 

675 

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

677 self.assertTrue(skyWcsCopy.isFits) 

678 self.checkMakeFlippedWcs(skyWcs) 

679 

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

681 def testAgainstAstropyWcs(self): 

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

683 header = makeLimitedFitsHeader(self.metadata) 

684 astropyWcs = astropy.wcs.WCS(header) 

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

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

687 

688 def testLinearizeMethods(self): 

689 skyWcs = makeSkyWcs(self.metadata) 

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

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

692 pix00 = skyWcs.skyToPixel(sky00) 

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

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

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

696 linSkyToPix1 = skyWcs.linearizeSkyToPixel(sky00, skyUnit) 

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

698 

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

700 linSky = linPixToSky1(pixel) 

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

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

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

704 

705 sky00Doubles = sky00.getPosition(skyUnit) 

706 pix00gApprox = linSkyToPix1(sky00Doubles) 

707 self.assertPairsAlmostEqual(pix00gApprox, pix00) 

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

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

710 pixelScale = skyWcs.getPixelScale(pix00) 

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

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

713 self.assertAlmostEqual(pixelArea, predictedPixelArea) 

714 

715 def testBasics(self): 

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

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

718 self.checkWcs(skyWcs) 

719 makeSkyWcs(self.metadata, strip=True) 

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

721 

722 def testBasicsStrip(self): 

723 stripWcsMetadata(self.metadata) 

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

725 # The metadata should be unchanged if we attempt to strip it again 

726 metadataCopy = self.metadata.deepCopy() 

727 stripWcsMetadata(self.metadata) 

728 for key in self.metadata.keys(): 

729 self.assertEqual(self.metadata[key], metadataCopy[key]) 

730 

731 def testNormalizationFk5(self): 

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

733 """ 

734 equinox = 1975.0 

735 metadata = self.metadata 

736 

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

738 metadata.set("EQUINOX", equinox) 

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

740 # record the original CRVAL before reading and stripping metadata 

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

742 

743 # create the wcs and retrieve crval 

744 skyWcs = makeSkyWcs(metadata) 

745 crval = skyWcs.getSkyOrigin() 

746 

747 # compare to computed crval 

748 computedCrval = skyWcs.pixelToSky(crpix) 

749 self.assertSpherePointsAlmostEqual(crval, computedCrval) 

750 

751 # get predicted crval by converting with astropy 

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

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

754 predictedCrvalIcrs = crvalFk5.icrs 

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

756 lsst.geom.radians) 

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

758 

759 def testNormalizationDecRa(self): 

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

761 """ 

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

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

764 

765 # swap RA, Decaxes in metadata 

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

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

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

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

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

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

772 

773 # create the wcs 

774 skyWcs = makeSkyWcs(self.metadata) 

775 

776 # compare pixel origin to input crval 

777 crval = skyWcs.getSkyOrigin() 

778 self.assertSpherePointsAlmostEqual(crval, crvalIn) 

779 

780 # compare to computed crval 

781 computedCrval = skyWcs.pixelToSky(crpix) 

782 self.assertSpherePointsAlmostEqual(crval, computedCrval) 

783 

784 def testReadDESHeader(self): 

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

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

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

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

789 

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

791 self.checkWcs(skyWcs) 

792 

793 def testCD_PC(self): 

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

795 md = PropertyList() 

796 for k, v in ( 

797 ("EQUINOX", 2000.0), 

798 ("RADESYS", "ICRS"), 

799 ("CRPIX1", 5353.0), 

800 ("CRPIX2", -35.0), 

801 ("CD1_1", 0.0), 

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

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

804 ("CD2_2", -0.0), 

805 ("CRVAL1", 4.5789875), 

806 ("CRVAL2", 16.30004444), 

807 ("CUNIT1", "deg"), 

808 ("CUNIT2", "deg"), 

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

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

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

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

813 ): 

814 md.set(k, v) 

815 

816 wcs = makeSkyWcs(md, strip=False) 

817 

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

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

820 

821 skyPos = wcs.pixelToSky(pixPos) 

822 self.assertSpherePointsAlmostEqual(skyPos, pred_skyPos) 

823 

824 for badPC in (False, True): 

825 for k, v in ( 

826 ("PC001001", 0.0), 

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

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

829 ("PC002002", 0.0), 

830 ): 

831 md.set(k, v) 

832 

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

834 if not badPC: 

835 for i in (1, 2,): 

836 for j in (1, 2,): 

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

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

839 

840 wcs2 = makeSkyWcs(md, strip=False) 

841 skyPos2 = wcs2.pixelToSky(pixPos) 

842 self.assertSpherePointsAlmostEqual(skyPos2, pred_skyPos) 

843 

844 def testNoEpoch(self): 

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

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

847 skyWcs = makeSkyWcs(self.metadata) 

848 header = skyWcs.getFitsMetadata() 

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

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

851 

852 def testCdMatrix(self): 

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

854 self.metadata.remove("CD1_2") 

855 self.metadata.remove("CD2_1") 

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

857 header = skyWcs.getFitsMetadata() 

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

859 # There's some mild rounding going on 

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

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

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

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

864 

865 

866class TestTanSipTestCase(SkyWcsBaseTestCase): 

867 

868 def setUp(self): 

869 metadata = PropertyList() 

870 # the following was fit using CreateWcsWithSip from meas_astrom 

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

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

873 for name, value in ( 

874 ("RADESYS", "ICRS"), 

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

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

877 ("CRPIX1", 1531.1824767147), 

878 ("CRPIX2", 1531.1824767147), 

879 ("CRVAL1", 43.035511801383), 

880 ("CRVAL2", 44.305697682784), 

881 ("CUNIT1", "deg"), 

882 ("CUNIT2", "deg"), 

883 ("CD1_1", 0.00027493991598151), 

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

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

886 ("CD2_2", 0.00027493937506632), 

887 ("A_ORDER", 5), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

906 ("B_ORDER", 5), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

925 ("AP_ORDER", 6), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

954 ("BP_ORDER", 6), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

983 ): 

984 metadata.set(name, value) 

985 self.metadata = metadata 

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

987 

988 def testTanSipFromFrameDict(self): 

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

990 """ 

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

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

993 

994 def testFitsMetadata(self): 

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

996 """ 

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

998 self.assertTrue(skyWcs.isFits) 

999 fitsMetadata = skyWcs.getFitsMetadata(precise=True) 

1000 skyWcsCopy = makeSkyWcs(fitsMetadata) 

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

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

1003 

1004 def testGetIntermediateWorldCoordsToSky(self): 

1005 """Test getIntermediateWorldCoordsToSky and getPixelToIntermediateWorldCoords 

1006 """ 

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

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

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

1010 for simplify in (False, True): 

1011 pixelToIwc = getPixelToIntermediateWorldCoords(skyWcs, simplify) 

1012 iwcToSky = getIntermediateWorldCoordsToSky(skyWcs, simplify) 

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

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

1015 if simplify: 

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

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

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

1019 # so don't assert isSimple is false 

1020 

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

1022 # in the forward and inverse direction 

1023 pixPosList = [] 

1024 for dx in (0, 1000): 

1025 for dy in (0, 1000): 

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

1027 iwcPosList = pixelToIwc.applyForward(pixPosList) 

1028 skyPosList = iwcToSky.applyForward(iwcPosList) 

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

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

1031 skyWcs.skyToPixel(skyPosList)) 

1032 

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

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

1035 

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

1037 ourPixelToIwc = makeSipPixelToIwc(self.metadata) 

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

1039 ourPixelToIwc.applyForward(pixPosList)) 

1040 

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

1042 ourIwcToPixel = makeSipIwcToPixel(self.metadata) 

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

1044 ourIwcToPixel.applyForward(iwcPosList)) 

1045 

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

1047 def testAgainstAstropyWcs(self): 

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

1049 header = makeLimitedFitsHeader(self.metadata) 

1050 astropyWcs = astropy.wcs.WCS(header) 

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

1052 

1053 def testMakeTanSipWcs(self): 

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

1055 

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

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

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

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

1060 cdMatrix = getCdMatrixFromMetadata(self.metadata) 

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

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

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

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

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

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

1067 sipAp=sipAp, sipBp=sipBp) 

1068 

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

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

1071 self.checkMakeFlippedWcs(skyWcs1) 

1072 self.checkMakeFlippedWcs(skyWcs2) 

1073 

1074 def testReadWriteFits(self): 

1075 wcsFromMetadata = makeSkyWcs(self.metadata) 

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

1077 wcsFromMetadata.writeFits(filePath) 

1078 wcsFromFits = SkyWcs.readFits(filePath) 

1079 

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

1081 maxDiffSky=0*lsst.geom.radians) 

1082 

1083 def testReadOldTanSipFits(self): 

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

1085 

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

1087 """ 

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

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

1090 wcsFromFits = SkyWcs.readFits(filePath) 

1091 

1092 wcsFromMetadata = makeSkyWcs(self.metadata) 

1093 

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

1095 self.assertWcsAlmostEqualOverBBox(wcsFromFits, wcsFromMetadata, bbox) 

1096 

1097 def testReadOldTanFits(self): 

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

1099 

1100 That file was made using the same metadata follows 

1101 (like self.metadata without the distortion) 

1102 """ 

1103 tanMetadata = PropertyList() 

1104 # the following was fit using CreateWcsWithSip from meas_astrom 

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

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

1107 for name, value in ( 

1108 ("RADESYS", "ICRS"), 

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

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

1111 ("CRPIX1", 1531.1824767147), 

1112 ("CRPIX2", 1531.1824767147), 

1113 ("CRVAL1", 43.035511801383), 

1114 ("CRVAL2", 44.305697682784), 

1115 ("CUNIT1", "deg"), 

1116 ("CUNIT2", "deg"), 

1117 ("CD1_1", 0.00027493991598151), 

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

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

1120 ("CD2_2", 0.00027493937506632), 

1121 ): 

1122 tanMetadata.set(name, value) 

1123 

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

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

1126 wcsFromFits = SkyWcs.readFits(filePath) 

1127 

1128 wcsFromMetadata = makeSkyWcs(tanMetadata) 

1129 

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

1131 self.assertWcsAlmostEqualOverBBox(wcsFromFits, wcsFromMetadata, bbox) 

1132 

1133 

1134class WcsPairTransformTestCase(SkyWcsBaseTestCase): 

1135 """Test functionality of makeWcsPairTransform. 

1136 """ 

1137 def setUp(self): 

1138 SkyWcsBaseTestCase.setUp(self) 

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

1140 crvalList = [ 

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

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

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

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

1145 ] 

1146 orientationList = [ 

1147 0 * lsst.geom.degrees, 

1148 0.00001 * lsst.geom.degrees, 

1149 -0.00001 * lsst.geom.degrees, 

1150 -45 * lsst.geom.degrees, 

1151 90 * lsst.geom.degrees, 

1152 ] 

1153 scale = 1.0 * lsst.geom.arcseconds 

1154 

1155 self.wcsList = [] 

1156 for crval in crvalList: 

1157 for orientation in orientationList: 

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

1159 self.wcsList.append(makeSkyWcs( 

1160 crpix=crpix, 

1161 crval=crval, 

1162 cdMatrix=cd)) 

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

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

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

1166 

1167 def testGenericWcs(self): 

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

1169 

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

1171 """ 

1172 inPoints = self.pixelPoints 

1173 for wcs1 in self.wcsList: 

1174 for wcs2 in self.wcsList: 

1175 transform = makeWcsPairTransform(wcs1, wcs2) 

1176 outPoints = transform.applyForward(inPoints) 

1177 inPointsRoundTrip = transform.applyInverse(outPoints) 

1178 self.assertPairListsAlmostEqual(inPoints, inPointsRoundTrip) 

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

1180 wcs2.pixelToSky(outPoints)) 

1181 

1182 def testSameWcs(self): 

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

1184 """ 

1185 for wcs in self.wcsList: 

1186 transform = makeWcsPairTransform(wcs, wcs) 

1187 # check that the transform has been simplified 

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

1189 # check the transform 

1190 outPoints1 = transform.applyForward(self.pixelPoints) 

1191 outPoints2 = transform.applyInverse(outPoints1) 

1192 self.assertPairListsAlmostEqual(self.pixelPoints, outPoints1) 

1193 self.assertPairListsAlmostEqual(outPoints1, outPoints2) 

1194 

1195 

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

1197 pass 

1198 

1199 

1200def setup_module(module): 

1201 lsst.utils.tests.init() 

1202 

1203 

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

1205 lsst.utils.tests.init() 

1206 unittest.main()