Skip to content

Commit a69ae65

Browse files
committed
Add unit tests that use gwcs
1 parent a6fd81a commit a69ae65

File tree

3 files changed

+160
-11
lines changed

3 files changed

+160
-11
lines changed

drizzle/tests/helpers.py

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,56 @@
22

33
import numpy as np
44

5-
from astropy import wcs
5+
from astropy import coordinates as coord
6+
from astropy import wcs as fits_wcs
67
from astropy.io import fits
8+
from astropy.modeling.models import (
9+
Shift, Polynomial2D, Pix2Sky_TAN, RotateNative2Celestial, Mapping
10+
)
11+
from astropy.modeling.projections import AffineTransformation2D
12+
from astropy import units
13+
14+
from gwcs.coordinate_frames import CelestialFrame, Frame2D
15+
import gwcs
716

817
__all__ = ["wcs_from_file"]
918

1019
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
1120
DATA_DIR = os.path.join(TEST_DIR, 'data')
1221

1322

14-
def wcs_from_file(filename, ext=None, return_data=False, crpix_shift=None):
23+
def wcs_from_file(filename, ext=None, return_data=False, crpix_shift=None,
24+
wcs_type="fits"):
1525
"""
1626
Read the WCS from a ".fits" file.
27+
28+
Parameters
29+
----------
30+
31+
filename : str
32+
Name of the file to load WCS from.
33+
34+
ext : int, None, optional
35+
Extension number to load the WCS from. When `None`, the WCS will be
36+
loaded from the first extension containing a WCS.
37+
38+
return_data : bool, optional
39+
When `True`, this function will return a tuple with first item
40+
being the WCS and the second item being the image data array.
41+
42+
crpix_shift : tuple, None, optional
43+
A tuple of two values to be added to header CRPIX values before
44+
creating the WCS. This effectively introduces a constant shift
45+
in the image coordinate system.
46+
47+
wcs_type : {"fits", "gwcs"}, optional
48+
Return either a FITS WCS or a gwcs.
49+
50+
Returns
51+
-------
52+
53+
WCS or tuple of WCS and image data
54+
1755
"""
1856
full_file_name = os.path.join(DATA_DIR, filename)
1957
path = os.path.join(DATA_DIR, full_file_name)
@@ -38,9 +76,12 @@ def wcs_from_file(filename, ext=None, return_data=False, crpix_shift=None):
3876
hdr["CRPIX1"] += crpix_shift[0]
3977
hdr["CRPIX2"] += crpix_shift[1]
4078

41-
result = wcs.WCS(hdr, hdu)
79+
result = fits_wcs.WCS(hdr, hdu)
4280
result.array_shape = shape
4381

82+
if wcs_type == "gwcs":
83+
result = _gwcs_from_hst_fits_wcs(result)
84+
4485
if return_data:
4586
result = (result, )
4687
if not isinstance(return_data, (list, tuple)):
@@ -50,3 +91,99 @@ def wcs_from_file(filename, ext=None, return_data=False, crpix_shift=None):
5091
result = result + data
5192

5293
return result
94+
95+
96+
def _gwcs_from_hst_fits_wcs(w):
97+
# NOTE: this function ignores table distortions
98+
def coeffs_to_poly(mat, degree):
99+
pol = Polynomial2D(degree=degree)
100+
for i in range(mat.shape[0]):
101+
for j in range(mat.shape[1]):
102+
if 0 < i + j <= degree:
103+
setattr(pol, f'c{i}_{j}', mat[i, j])
104+
return pol
105+
106+
nx, ny = w.pixel_shape
107+
x0, y0 = w.wcs.crpix - 1
108+
109+
cd = w.wcs.piximg_matrix
110+
111+
if w.sip is None:
112+
# construct GWCS:
113+
det2sky = (
114+
(Shift(-x0) & Shift(-y0)) |
115+
Pix2Sky_TAN() | RotateNative2Celestial(*w.wcs.crval, 180)
116+
)
117+
else:
118+
cfx, cfy = np.dot(cd, [w.sip.a.ravel(), w.sip.b.ravel()])
119+
a = np.reshape(cfx, w.sip.a.shape)
120+
b = np.reshape(cfy, w.sip.b.shape)
121+
a[1, 0] = cd[0, 0]
122+
a[0, 1] = cd[0, 1]
123+
b[1, 0] = cd[1, 0]
124+
b[0, 1] = cd[1, 1]
125+
126+
polx = coeffs_to_poly(a, w.sip.a_order)
127+
poly = coeffs_to_poly(b, w.sip.b_order)
128+
129+
sip = Mapping((0, 1, 0, 1)) | (polx & poly)
130+
131+
# construct GWCS:
132+
det2sky = (
133+
(Shift(-x0) & Shift(-y0)) | sip |
134+
Pix2Sky_TAN() | RotateNative2Celestial(*w.wcs.crval, 180)
135+
)
136+
137+
detector_frame = Frame2D(
138+
name="detector",
139+
axes_names=("x", "y"),
140+
unit=(units.pix, units.pix)
141+
)
142+
sky_frame = CelestialFrame(
143+
reference_frame=getattr(coord, w.wcs.radesys).__call__(),
144+
name=w.wcs.radesys,
145+
unit=(units.deg, units.deg)
146+
)
147+
pipeline = [(detector_frame, det2sky), (sky_frame, None)]
148+
gw = gwcs.wcs.WCS(pipeline)
149+
gw.array_shape = w.array_shape
150+
gw.bounding_box = ((-0.5, nx - 0.5), (-0.5, ny - 0.5))
151+
152+
if w.sip is not None:
153+
# compute inverse SIP and re-create output GWCS
154+
155+
# compute inverse SIP:
156+
hdr = gw.to_fits_sip(
157+
max_inv_pix_error=1e-5,
158+
inv_degree=None,
159+
npoints=64,
160+
crpix=w.wcs.crpix,
161+
projection='TAN',
162+
verbose=False
163+
)
164+
winv = fits_wcs.WCS(hdr)
165+
ap = winv.sip.ap.copy()
166+
bp = winv.sip.bp.copy()
167+
ap[1, 0] += 1
168+
bp[0, 1] += 1
169+
polx_inv = coeffs_to_poly(ap, winv.sip.ap_order)
170+
poly_inv = coeffs_to_poly(bp, winv.sip.bp_order)
171+
af = AffineTransformation2D(
172+
matrix=np.linalg.inv(winv.wcs.piximg_matrix)
173+
)
174+
175+
# set analytical inverses:
176+
sip.inverse = af | Mapping((0, 1, 0, 1)) | (polx_inv & poly_inv)
177+
178+
# construct GWCS:
179+
det2sky = (
180+
(Shift(-x0) & Shift(-y0)) | sip |
181+
Pix2Sky_TAN() | RotateNative2Celestial(*w.wcs.crval, 180)
182+
)
183+
184+
pipeline = [(detector_frame, det2sky), (sky_frame, None)]
185+
gw = gwcs.wcs.WCS(pipeline)
186+
gw.array_shape = w.array_shape
187+
gw.bounding_box = ((-0.5, nx - 0.5), (-0.5, ny - 0.5))
188+
189+
return gw

drizzle/tests/test_utils.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import pytest
33
from numpy.testing import assert_almost_equal, assert_equal
4+
import gwcs
45

56
from drizzle.utils import (
67
_estimate_pixel_scale,
@@ -25,14 +26,17 @@ def test_map_rectangular():
2526
assert_equal(pixmap[5, 500], (500, 5))
2627

2728

28-
def test_map_to_self():
29+
@pytest.mark.parametrize(
30+
"wcs_type", ["fits", "gwcs"]
31+
)
32+
def test_map_to_self(wcs_type):
2933
"""
3034
Map a pixel array to itself. Should return the same array.
3135
"""
32-
input_wcs = wcs_from_file("j8bt06nyq_sip_flt.fits", ext=1)
36+
input_wcs = wcs_from_file("j8bt06nyq_sip_flt.fits", ext=1, wcs_type=wcs_type)
3337
shape = input_wcs.array_shape
3438

35-
ok_pixmap = np.indices(shape, dtype='float32')
39+
ok_pixmap = np.indices(shape, dtype='float64')
3640
ok_pixmap = ok_pixmap.transpose()
3741

3842
pixmap = calc_pixmap(input_wcs, input_wcs)
@@ -68,17 +72,24 @@ def test_map_to_self():
6872
assert_equal(pixmap.shape, ok_pixmap.shape)
6973

7074

71-
def test_translated_map():
75+
@pytest.mark.parametrize(
76+
"wcs_type", ["fits", "gwcs"]
77+
)
78+
def test_translated_map(wcs_type):
7279
"""
7380
Map a pixel array to at translated array.
7481
"""
75-
first_wcs = wcs_from_file("j8bt06nyq_sip_flt.fits", ext=1)
82+
first_wcs = wcs_from_file(
83+
"j8bt06nyq_sip_flt.fits",
84+
ext=1,
85+
wcs_type=wcs_type
86+
)
7687
second_wcs = wcs_from_file(
7788
"j8bt06nyq_sip_flt.fits",
7889
ext=1,
79-
crpix_shift=(-2, -2) # shift loaded WCS by subtracting this from CRPIX
90+
crpix_shift=(-2, -2), # shift loaded WCS by adding this to CRPIX
91+
wcs_type=wcs_type
8092
)
81-
assert np.allclose(second_wcs.wcs.crpix, (510, 510))
8293

8394
ok_pixmap = np.indices(first_wcs.array_shape, dtype='float32') - 2.0
8495
ok_pixmap = ok_pixmap.transpose()
@@ -88,7 +99,7 @@ def test_translated_map():
8899
# Got x-y transpose right
89100
assert_equal(pixmap.shape, ok_pixmap.shape)
90101
# Mapping an array to a translated array
91-
assert_almost_equal(pixmap, ok_pixmap, decimal=5)
102+
assert_almost_equal(pixmap[2:, 2:], ok_pixmap[2:, 2:], decimal=5)
92103

93104

94105
def test_estimate_pixel_scale_ratio():

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Documentation = "http://spacetelescope.github.io/drizzle/"
2929
[project.optional-dependencies]
3030
test = [
3131
"astropy",
32+
"gwcs",
3233
"pytest",
3334
"pytest-cov",
3435
"pytest-doctestplus",

0 commit comments

Comments
 (0)