diff --git a/CHANGES.rst b/CHANGES.rst index 08c5e3fe30..c7f6e70265 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,6 +24,9 @@ assign_wcs data file to properly handle background and virtual slits, and assign appropriate meta data to them for use downstream. [#8442, #8533] +- Update default parameters to increase the accuracy of the SIP approximation + in the output FITS WCS. [#8529] + associations ------------ @@ -252,7 +255,10 @@ tweakreg - Improve error handling in the absolute alignment. [#8450, #8477] - Change code default to use IRAF StarFinder instead of - DAO StarFinder [#8487] + DAO StarFinder. [#8487] + +- Add new step parameters to control SIP approximation in the output FITS WCS, + matching the default values used in the ``assign_wcs`` step. [#8529] - Added a check for ``(abs_)separation`` and ``(abs_)tolerance`` parameters that ``separation`` > ``sqrt(2) * tolerance`` that will now log an error diff --git a/docs/jwst/assign_wcs/arguments.rst b/docs/jwst/assign_wcs/arguments.rst index 9ec3b7af9b..197bd83d21 100644 --- a/docs/jwst/assign_wcs/arguments.rst +++ b/docs/jwst/assign_wcs/arguments.rst @@ -11,14 +11,14 @@ the behavior of the processing. ``--sip_degree`` (integer, max=6, default=None) Polynomial degree for the forward SIP fit. "None" uses the best fit. -``--sip_max_pix_error`` (float, default=0.1) +``--sip_max_pix_error`` (float, default=0.01) Maximum error for the SIP forward fit, in units of pixels. Ignored if ``sip_degree`` is set to an explicit value. ``--sip_inv_degree`` (integer, max=6, default=None) Polynomial degree for the inverse SIP fit. "None" uses the best fit. -``--sip_max_inv_pix_error`` (float, default=0.1) +``--sip_max_inv_pix_error`` (float, default=0.01) Maximum error for the SIP inverse fit, in units of pixels. Ignored if ``sip_inv_degree`` is set to an explicit value. diff --git a/docs/jwst/assign_wcs/main.rst b/docs/jwst/assign_wcs/main.rst index f2265c9a51..e865b8e197 100644 --- a/docs/jwst/assign_wcs/main.rst +++ b/docs/jwst/assign_wcs/main.rst @@ -30,9 +30,11 @@ create and populate the WCS object for the exposure. For image display with software like DS9 that relies on specific WCS information, a SIP-based approximation to the WCS is fit. The results are FITS keywords stored in -``model.meta.wcsinfo``. This is not an exact fit, but is accurate to ~0.25 pixel and is sufficient -for display purposes. This step, which occurs for imaging modes, is performed by default, but -can be switched off, and parameters controlling the SIP fit can also be adjusted. +``model.meta.wcsinfo``. This is not an exact fit, but is accurate to ~0.01 pixel by default, +and is sufficient for display purposes. This step, which occurs for imaging modes, is +performed by default, but can be switched off, and parameters controlling the SIP fit can +also be adjusted. Note that if these parameters are changed, the equivalent parameters +for the ``tweakreg`` step should be adjusted to match. The ``assign_wcs`` step can accept either a ``rate`` product, which is the result of averaging over all integrations in an exposure, or a ``rateints`` product, which is a 3D cube of diff --git a/docs/jwst/tweakreg/README.rst b/docs/jwst/tweakreg/README.rst index f24aa60f2d..8d5315e428 100644 --- a/docs/jwst/tweakreg/README.rst +++ b/docs/jwst/tweakreg/README.rst @@ -393,7 +393,7 @@ Parameters used for absolute astrometry to a reference catalog. that apply to ``fitgeometry`` also apply to ``abs_fitgeometry``. * ``abs_nclip``: A non-negative integer number of clipping iterations - to use in the fit. (Default = 3) + to use in the fit. (Default=3) * ``abs_sigma``: A positive `float` indicating the clipping limit, in sigma units, used when performing fit. (Default=3.0) @@ -401,6 +401,32 @@ Parameters used for absolute astrometry to a reference catalog. * ``save_abs_catalog``: A boolean specifying whether or not to write out the astrometric catalog used for the fit as a separate product. (Default=False) +**SIP approximation parameters:** + +Parameters used to provide a SIP-based approximation to the WCS, +for FITS display. These parameter values should match the ones used +in the ``assign_wcs`` step. + +* ``sip_approx``: A boolean flag to enable the computation of a SIP + approximation. (Default=True) + +* ``sip_degree``: A positive `int`, specifying the polynomial degree for + the forward SIP fit. `None` uses the best fit; the maximum value allowed + is 6. (Default=None) + +* ``sip_max_pix_error``: A positive `float`, specifying the maximum + error for the SIP forward fit, in units of pixels. Ignored if + ``sip_degree`` is set to an explicit value. (Default=0.01) + +* ``sip_inv_degree``: A positive `int`, specifying the polynomial degree for + the inverse SIP fit. `None` uses the best fit; the maximum value allowed + is 6. (Default=None) + +* ``sip_max_inv_pix_error``: A positive `float`, specifying the maximum + error for the SIP inverse fit, in units of pixels. Ignored if + ``sip_inv_degree`` is set to an explicit value. (Default=0.01) + +* ``sip_npoints``: Number of points for the SIP fit. (Default=12). Further Documentation --------------------- diff --git a/jwst/assign_wcs/assign_wcs_step.py b/jwst/assign_wcs/assign_wcs_step.py index b43d4a9c5c..324bef826d 100755 --- a/jwst/assign_wcs/assign_wcs_step.py +++ b/jwst/assign_wcs/assign_wcs_step.py @@ -52,9 +52,9 @@ class AssignWcsStep(Step): spec = """ sip_approx = boolean(default=True) # enables SIP approximation for imaging modes. - sip_max_pix_error = float(default=0.1) # max err for SIP fit, forward. + sip_max_pix_error = float(default=0.01) # max err for SIP fit, forward. sip_degree = integer(max=6, default=None) # degree for forward SIP fit, None to use best fit. - sip_max_inv_pix_error = float(default=0.1) # max err for SIP fit, inverse. + sip_max_inv_pix_error = float(default=0.01) # max err for SIP fit, inverse. sip_inv_degree = integer(max=6, default=None) # degree for inverse SIP fit, None to use best fit. sip_npoints = integer(default=12) # number of points for SIP slit_y_low = float(default=-.55) # The lower edge of a slit. @@ -131,6 +131,8 @@ def process(self, input, *args, **kwargs): wfss_imaging_wcs(result, imaging_func, bbox=bbox, max_pix_error=self.sip_max_pix_error, degree=self.sip_degree, + max_inv_pix_error=self.sip_max_inv_pix_error, + inv_degree=self.sip_inv_degree, npoints=self.sip_npoints, ) except (ValueError, RuntimeError) as e: diff --git a/jwst/assign_wcs/tests/test_wcs.py b/jwst/assign_wcs/tests/test_wcs.py index 7f057263b4..cd1b4e9912 100644 --- a/jwst/assign_wcs/tests/test_wcs.py +++ b/jwst/assign_wcs/tests/test_wcs.py @@ -248,7 +248,9 @@ def test_sip_approx(tmp_path): im = ImageModel(hdu1) pipe = AssignWcsStep() - result = pipe.call(im) + result = pipe.call(im, sip_max_pix_error=0.1, sip_degree=3, + sip_max_inv_pix_error=0.1, sip_inv_degree=3, + sip_npoints=12) # check that result.meta.wcsinfo has correct # values after SIP approx. @@ -278,4 +280,4 @@ def test_sip_approx(tmp_path): result.write(path) with open(path) as result_read: - result.meta.wcsinfo == result_read.meta.wcsinfo + assert result.meta.wcsinfo == result_read.meta.wcsinfo diff --git a/jwst/assign_wcs/util.py b/jwst/assign_wcs/util.py index f9f90918b4..2767182fe0 100644 --- a/jwst/assign_wcs/util.py +++ b/jwst/assign_wcs/util.py @@ -1254,8 +1254,10 @@ def in_ifu_slice(slice_wcs, ra, dec, lam): return onslice_ind -def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, - crpix=None, projection='TAN', imwcs=None, **kwargs): +def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, + max_inv_pix_error=0.01, inv_degree=None, + npoints=12, crpix=None, projection='TAN', + imwcs=None, **kwargs): """ Update ``datamodel.meta.wcsinfo`` based on a FITS WCS + SIP approximation of a GWCS object. By default, this function will approximate @@ -1307,6 +1309,23 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, to be fit to the WCS transformation. In this case ``max_pixel_error`` is ignored. + max_inv_pix_error : float, None, optional + Maximum allowed inverse error over the domain of the pixel array + in pixel units. With the default value of `None` no inverse + is generated. + + inv_degree : int, iterable, None, optional + Degree of the SIP polynomial. Default value `None` indicates that + all allowed degree values (``[1...6]``) will be considered and + the lowest degree that meets accuracy requerements set by + ``max_pix_error`` will be returned. Alternatively, ``degree`` can be + an iterable containing allowed values for the SIP polynomial degree. + This option is similar to default `None` but it allows caller to + restrict the range of allowed SIP degrees used for fitting. + Finally, ``degree`` can be an integer indicating the exact SIP degree + to be fit to the WCS transformation. In this case + ``max_inv_pixel_error`` is ignored. + npoints : int, optional The number of points in each dimension to sample the bounding box for use in the SIP fit. Minimum number of points is 3. @@ -1346,24 +1365,6 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, Other Parameters ---------------- - - max_inv_pix_error : float, None, optional - Maximum allowed inverse error over the domain of the pixel array - in pixel units. With the default value of `None` no inverse - is generated. - - inv_degree : int, iterable, None, optional - Degree of the SIP polynomial. Default value `None` indicates that - all allowed degree values (``[1...6]``) will be considered and - the lowest degree that meets accuracy requerements set by - ``max_pix_error`` will be returned. Alternatively, ``degree`` can be - an iterable containing allowed values for the SIP polynomial degree. - This option is similar to default `None` but it allows caller to - restrict the range of allowed SIP degrees used for fitting. - Finally, ``degree`` can be an integer indicating the exact SIP degree - to be fit to the WCS transformation. In this case - ``max_inv_pixel_error`` is ignored. - bounding_box : tuple, None, optional A pair of tuples, each consisting of two numbers Represents the range of pixel values in both dimensions @@ -1372,12 +1373,10 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, verbose : bool, optional Print progress of fits. - Returns ------- FITS header with all SIP WCS keywords - Raises ------ ValueError @@ -1385,7 +1384,6 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, specified accuracy (both forward and inverse, both rms and maximum) is not achieved an exception will be raised. - Notes ----- @@ -1409,15 +1407,11 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, # make a copy of kwargs: kwargs = {k: v for k, v in kwargs.items()} - # override default values for "other parameters": - max_inv_pix_error = kwargs.pop('max_inv_pix_error', None) - inv_degree = kwargs.pop('inv_degree', None) - if inv_degree is None: - inv_degree = range(1, _MAX_SIP_DEGREE) - - # limit default 'degree' range to _MAX_SIP_DEGREE: + # limit default 'degree' ranges to _MAX_SIP_DEGREE: if degree is None: degree = range(1, _MAX_SIP_DEGREE) + if inv_degree is None: + inv_degree = range(1, _MAX_SIP_DEGREE) hdr = imwcs.to_fits_sip( max_pix_error=max_pix_error, @@ -1426,6 +1420,7 @@ def update_fits_wcsinfo(datamodel, max_pix_error=0.01, degree=None, npoints=32, inv_degree=inv_degree, npoints=npoints, crpix=crpix, + projection=projection, **kwargs ) diff --git a/jwst/tweakreg/tests/test_tweakreg.py b/jwst/tweakreg/tests/test_tweakreg.py index 47b071c337..da5e5bd894 100644 --- a/jwst/tweakreg/tests/test_tweakreg.py +++ b/jwst/tweakreg/tests/test_tweakreg.py @@ -1,18 +1,20 @@ -from copy import deepcopy import json import os +from copy import deepcopy import asdf -from astropy.modeling.models import Shift -from astropy.table import Table import numpy as np import pytest +from astropy.wcs import WCS +from astropy.modeling.models import Shift +from astropy.table import Table +from gwcs.wcstools import grid_from_bounding_box +from stdatamodels.jwst.datamodels import ImageModel +from jwst.datamodels import ModelContainer from jwst.tweakreg import tweakreg_step from jwst.tweakreg import tweakreg_catalog from jwst.tweakreg.utils import _wcsinfo_from_wcs_transform -from stdatamodels.jwst.datamodels import ImageModel -from jwst.datamodels import ModelContainer BKG_LEVEL = 0.001 @@ -340,3 +342,59 @@ def patched_construct_wcs_corrector(model, catalog, _seen=[]): with pytest.raises(ValueError, match="done testing"): step(str(asn_path)) + +@pytest.mark.parametrize("with_shift", [True, False]) +def test_sip_approx(example_input, with_shift): + """ + Test the output FITS WCS. + """ + if with_shift: + # shift 9 pixels so that the sources in one of the 2 images + # appear at different locations (resulting in a correct wcs update) + example_input[1].data[:-9] = example_input[1].data[9:] + example_input[1].data[-9:] = BKG_LEVEL + + # assign images to different groups (so they are aligned to each other) + example_input[0].meta.group_id = 'a' + example_input[1].meta.group_id = 'b' + + # call th step with override SIP approximation parameters + step = tweakreg_step.TweakRegStep() + step.sip_approx = True + step.sip_max_pix_error = 0.1 + step.sip_degree = 3 + step.sip_max_inv_pix_error = 0.1 + step.sip_inv_degree = 3 + step.sip_npoints = 12 + + # run the step on the example input modified above + result = step(example_input) + + # output wcs differs by a small amount due to the shift above: + # project one point through each wcs and compare the difference + abs_delta = abs(result[1].meta.wcs(0, 0)[0] - result[0].meta.wcs(0, 0)[0]) + if with_shift: + assert abs_delta > 1E-5 + else: + assert abs_delta < 1E-12 + + # the first wcs is identical to the input and + # does not have SIP approximation keywords -- + # they are normally set by assign_wcs + assert np.allclose(result[0].meta.wcs(0, 0)[0], example_input[0].meta.wcs(0, 0)[0]) + for key in ['ap_order', 'bp_order']: + assert key not in result[0].meta.wcsinfo.instance + + # for the second, SIP approximation should be present + for key in ['ap_order', 'bp_order']: + assert result[1].meta.wcsinfo.instance[key] == 3 + + # evaluate fits wcs and gwcs for the approximation, make sure they agree + wcs_info = result[1].meta.wcsinfo.instance + grid = grid_from_bounding_box(result[1].meta.wcs.bounding_box) + gwcs_ra, gwcs_dec = result[1].meta.wcs(*grid) + fits_wcs = WCS(wcs_info) + fitswcs_res = fits_wcs.pixel_to_world(*grid) + + assert np.allclose(fitswcs_res.ra.deg, gwcs_ra) + assert np.allclose(fitswcs_res.dec.deg, gwcs_dec) diff --git a/jwst/tweakreg/tweakreg_step.py b/jwst/tweakreg/tweakreg_step.py index 6b9fef82b7..d928cb166a 100644 --- a/jwst/tweakreg/tweakreg_step.py +++ b/jwst/tweakreg/tweakreg_step.py @@ -119,6 +119,14 @@ class TweakRegStep(Step): abs_separation = float(default=1) # Minimum object separation in arcsec when performing absolute astrometry abs_tolerance = float(default=0.7) # Matching tolerance for xyxymatch in arcsec when performing absolute astrometry + # SIP approximation options, should match assign_wcs + sip_approx = boolean(default=True) # enables SIP approximation for imaging modes. + sip_max_pix_error = float(default=0.01) # max err for SIP fit, forward. + sip_degree = integer(max=6, default=None) # degree for forward SIP fit, None to use best fit. + sip_max_inv_pix_error = float(default=0.01) # max err for SIP fit, inverse. + sip_inv_degree = integer(max=6, default=None) # degree for inverse SIP fit, None to use best fit. + sip_npoints = integer(default=12) # number of points for SIP + # stpipe general options output_use_model = boolean(default=True) # When saving use `DataModel.meta.filename` """ @@ -508,17 +516,22 @@ def process(self, input): # Also update FITS representation in input exposures for # subsequent reprocessing by the end-user. - try: - update_fits_wcsinfo( - image_model, - max_pix_error=0.01, - npoints=16 - ) - except (ValueError, RuntimeError) as e: - self.log.warning( - "Failed to update 'meta.wcsinfo' with FITS SIP " - f'approximation. Reported error is:\n"{e.args[0]}"' - ) + if self.sip_approx: + try: + update_fits_wcsinfo( + image_model, + max_pix_error=self.sip_max_pix_error, + degree=self.sip_degree, + max_inv_pix_error=self.sip_max_inv_pix_error, + inv_degree=self.sip_inv_degree, + npoints=self.sip_npoints, + crpix=None + ) + except (ValueError, RuntimeError) as e: + self.log.warning( + "Failed to update 'meta.wcsinfo' with FITS SIP " + f'approximation. Reported error is:\n"{e.args[0]}"' + ) return images