From 856ffba52c268b48553c7cdeb0b7d05972485dea Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 29 Jun 2023 11:30:55 -0400 Subject: [PATCH 001/117] Update .gitignore file. --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 62371214..fb79f2e8 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -143,4 +143,7 @@ src/stcal/_version.py # auto-generated API docs docs/source/api -.DS_Store \ No newline at end of file +.DS_Store + +# VSCode stuff +.vscode \ No newline at end of file From a4fed9219bc5f226c0f542e3dfdc373517c6f517 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 29 Jun 2023 11:33:41 -0400 Subject: [PATCH 002/117] Create subpackage and initial module structure. --- src/stcal/alignment/__init__.py | 0 src/stcal/alignment/astrometric_utils.py | 0 src/stcal/alignment/resample_utils.py | 0 src/stcal/alignment/tweakreg_catalog.py | 0 src/stcal/alignment/util.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/stcal/alignment/__init__.py create mode 100644 src/stcal/alignment/astrometric_utils.py create mode 100644 src/stcal/alignment/resample_utils.py create mode 100644 src/stcal/alignment/tweakreg_catalog.py create mode 100644 src/stcal/alignment/util.py diff --git a/src/stcal/alignment/__init__.py b/src/stcal/alignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/astrometric_utils.py b/src/stcal/alignment/astrometric_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/tweakreg_catalog.py b/src/stcal/alignment/tweakreg_catalog.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py new file mode 100644 index 00000000..e69de29b From f57851ab39ddc81782678d1bce94e27c0732c46b Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 30 Jun 2023 10:09:55 -0400 Subject: [PATCH 003/117] Add methods to alignment.util. --- src/stcal/alignment/util.py | 420 ++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index e69de29b..53f040b9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -0,0 +1,420 @@ +""" +Utility function for assign_wcs. + +""" +import logging +import functools +import numpy as np + +from astropy.coordinates import SkyCoord +from astropy.utils.misc import isiterable +from astropy import units as u +from astropy.modeling import models as astmodels +from typing import Union, List + +from gwcs import WCS +from gwcs import utils as gwutils +from gwcs.wcstools import wcs_from_fiducial + +from stdatamodels.jwst.datamodels import JwstDataModel +from roman_datamodels.datamodels import DataModel + + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +_MAX_SIP_DEGREE = 6 + + +__all__ = [ + "wcs_from_footprints", + "compute_scale", + "calc_rotation_matrix", +] + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : `~gwcs.wcs.WCS` + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def calc_rotation_matrix( + roll_ref: float, v3i_yang: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yang : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: [pc1_1, pc1_2, pc2_1, pc2_2] + The rotation matrix + + Notes + ----- + The rotation is + + ---------------- + | pc1_1 pc2_1 | + | pc1_2 pc2_2 | + ---------------- + + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yang) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + +def _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes +): + lon, lat = spatial_footprint + lon, lat = np.deg2rad(lon), np.deg2rad(lat) + x = np.cos(lat) * np.cos(lon) + y = np.cos(lat) * np.sin(lon) + z = np.sin(lat) + + x_mid = (np.max(x) + np.min(x)) / 2.0 + y_mid = (np.max(y) + np.min(y)) / 2.0 + z_mid = (np.max(z) + np.min(z)) / 2.0 + lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 + lat_fiducial = np.rad2deg( + np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) + ) + fiducial[spatial_axes] = lon_fiducial, lat_fiducial + + +def compute_fiducial(wcslist, bounding_box=None): + """ + For a celestial footprint this is the center. + For a spectral footprint, it is the beginning of the range. + + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def wcsinfo_from_model(input_model): + """ + Create a dict {wcs_keyword: array_of_values} pairs from a data model. + + Parameters + ---------- + input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` + The input data model + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes)) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial +): + wcsinfo = wcsinfo_from_model(refmodel) + if isinstance(refmodel, JwstDataModel): + sky_axes, spec, other = gwutils.get_axes(wcsinfo) + elif isinstance(refmodel, DataModel): + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + + # Need to put the rotation matrix (List[float, float, float, float]) + # returned from calc_rotation_matrix into the correct shape for + # constructing the transformation + v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) + vparity = refmodel.meta.wcsinfo.vparity + if rotation is None: + roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + else: + roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + + pc = np.reshape( + calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + ) + + rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") + transform = [rotation] + if sky_axes: + if not pscale: + pscale = compute_scale( + refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio + ) + transform.append( + astmodels.Scale(pscale, name="cdelt1") + & astmodels.Scale(pscale, name="cdelt2") + ) + + if transform: + transform = functools.reduce(lambda x, y: x | y, transform) + return transform + + +def wcs_from_footprints( + dmodels, + refmodel=None, + transform=None, + bounding_box=None, + pscale_ratio=None, + pscale=None, + rotation=None, + shape=None, + crpix=None, + crval=None, +): + """ + Create a WCS from a list of input data models. + + A fiducial point in the output coordinate frame is created from the + footprints of all WCS objects. For a spatial frame this is the center + of the union of the footprints. For a spectral frame the fiducial is in + the beginning of the footprint range. + If ``refmodel`` is None, the first WCS object in the list is considered + a reference. The output coordinate frame and projection (for celestial frames) + is taken from ``refmodel``. + If ``transform`` is not supplied, a compound transform is created using + CDELTs and PC. + If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed + from bounding_box of all input WCSs. + + Parameters + ---------- + dmodels : list of `~jwst.datamodels.JwstDataModel` + A list of data models. + refmodel : `~jwst.datamodels.JwstDataModel`, optional + This model's WCS is used as a reference. + WCS. The output coordinate frame, the projection and a + scaling and rotation transform is created from it. If not supplied + the first model in the list is used as ``refmodel``. + transform : `~astropy.modeling.core.Model`, optional + A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` + If not supplied Scaling | Rotation is computed from ``refmodel``. + bounding_box : tuple, optional + Bounding_box of the new WCS. + If not supplied it is computed from the bounding_box of all inputs. + pscale_ratio : float, None, optional + Ratio of input to output pixel scale. Ignored when either + ``transform`` or ``pscale`` are provided. + pscale : float, None, optional + Absolute pixel scale in degrees. When provided, overrides + ``pscale_ratio``. Ignored when ``transform`` is provided. + rotation : float, None, optional + Position angle of output image's Y-axis relative to North. + A value of 0.0 would orient the final output image to be North up. + The default of `None` specifies that the images will not be rotated, + but will instead be resampled in the default orientation for the camera + with the x and y axes of the resampled image corresponding + approximately to the detector axes. Ignored when ``transform`` is + provided. + shape : tuple of int, None, optional + Shape of the image (data array) using ``numpy.ndarray`` convention + (``ny`` first and ``nx`` second). This value will be assigned to + ``pixel_shape`` and ``array_shape`` properties of the returned + WCS object. + crpix : tuple of float, None, optional + Position of the reference pixel in the image array. If ``crpix`` is not + specified, it will be set to the center of the bounding box of the + returned WCS object. + crval : tuple of float, None, optional + Right ascension and declination of the reference pixel. Automatically + computed if not provided. + + """ + bb = bounding_box + wcslist = [im.meta.wcs for im in dmodels] + + if not isiterable(wcslist): + raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") + + if not all(isinstance(w, WCS) for w in wcslist): + raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") + + if refmodel is None: + refmodel = dmodels[0] + elif not isinstance(refmodel, (JwstDataModel, DataModel)): + raise TypeError("Expected refmodel to be an instance of DataModel.") + + fiducial = compute_fiducial(wcslist, bb) + if crval is not None: + # overwrite spatial axes with user-provided CRVAL: + i = 0 + for k, axt in enumerate(wcslist[0].output_frame.axes_type): + if axt == "SPATIAL": + fiducial[k] = crval[i] + i += 1 + + ref_fiducial = np.array( + [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + ) + + prj = astmodels.Pix2Sky_TAN() + + if transform is None: + transform = _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial + ) + + out_frame = refmodel.meta.wcs.output_frame + input_frame = refmodel.meta.wcs.input_frame + wnew = wcs_from_fiducial( + fiducial, + coordinate_frame=out_frame, + projection=prj, + transform=transform, + input_frame=input_frame, + ) + + footprints = [w.footprint().T for w in wcslist] + domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in out_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + if crpix is None: + offset1, offset2 = wnew.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] + else: + offset1, offset2 = crpix + offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" + ) + + wnew.insert_transform("detector", offsets, after=True) + wnew.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wnew.pixel_shape = shape[::-1] + wnew.array_shape = shape + + return wnew From 556279dfb032fa6049729e4144627b87b04fa070 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 5 Jul 2023 10:27:14 -0400 Subject: [PATCH 004/117] Update .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fb79f2e8..51a50d9b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +coverage/ # Translations *.mo From 1a3628e0c15d719f31ca5a69db383fd0fa9fbeb4 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 5 Jul 2023 10:32:04 -0400 Subject: [PATCH 005/117] Remove unused modules. --- src/stcal/alignment/astrometric_utils.py | 0 src/stcal/alignment/tweakreg_catalog.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/stcal/alignment/astrometric_utils.py delete mode 100644 src/stcal/alignment/tweakreg_catalog.py diff --git a/src/stcal/alignment/astrometric_utils.py b/src/stcal/alignment/astrometric_utils.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/stcal/alignment/tweakreg_catalog.py b/src/stcal/alignment/tweakreg_catalog.py deleted file mode 100644 index e69de29b..00000000 From a09e0cbe1f86bbf87dd854e5a38e1568f00624e9 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 6 Jul 2023 15:15:03 -0400 Subject: [PATCH 006/117] Clean module. --- src/stcal/alignment/util.py | 420 ------------------------------------ 1 file changed, 420 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 53f040b9..e69de29b 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -1,420 +0,0 @@ -""" -Utility function for assign_wcs. - -""" -import logging -import functools -import numpy as np - -from astropy.coordinates import SkyCoord -from astropy.utils.misc import isiterable -from astropy import units as u -from astropy.modeling import models as astmodels -from typing import Union, List - -from gwcs import WCS -from gwcs import utils as gwutils -from gwcs.wcstools import wcs_from_fiducial - -from stdatamodels.jwst.datamodels import JwstDataModel -from roman_datamodels.datamodels import DataModel - - -log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) - - -_MAX_SIP_DEGREE = 6 - - -__all__ = [ - "wcs_from_footprints", - "compute_scale", - "calc_rotation_matrix", -] - - -def compute_scale( - wcs: WCS, - fiducial: Union[tuple, np.ndarray], - disp_axis: int = None, - pscale_ratio: float = None, -) -> float: - """Compute scaling transform. - - Parameters - ---------- - wcs : `~gwcs.wcs.WCS` - Reference WCS object from which to compute a scaling factor. - - fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. - - disp_axis : int - Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` - - pscale_ratio : int - Ratio of input to output pixel scale - - Returns - ------- - scale : float - Scaling factor for x and y or cross-dispersion direction. - - """ - spectral = "SPECTRAL" in wcs.output_frame.axes_type - - if spectral and disp_axis is None: - raise ValueError("If input WCS is spectral, a disp_axis must be given") - - crpix = np.array(wcs.invert(*fiducial)) - - delta = np.zeros_like(crpix) - spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] - delta[spatial_idx[0]] = 1 - - crpix_with_offsets = np.vstack( - (crpix, crpix + delta, crpix + np.roll(delta, 1)) - ).T - crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) - - coords = SkyCoord( - ra=crval_with_offsets[spatial_idx[0]], - dec=crval_with_offsets[spatial_idx[1]], - unit="deg", - ) - xscale = np.abs(coords[0].separation(coords[1]).value) - yscale = np.abs(coords[0].separation(coords[2]).value) - - if pscale_ratio is not None: - xscale *= pscale_ratio - yscale *= pscale_ratio - - if spectral: - # Assuming scale doesn't change with wavelength - # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction - return yscale if disp_axis == 1 else xscale - - return np.sqrt(xscale * yscale) - - -def calc_rotation_matrix( - roll_ref: float, v3i_yang: float, vparity: int = 1 -) -> List[float]: - """Calculate the rotation matrix. - - Parameters - ---------- - roll_ref : float - Telescope roll angle of V3 North over East at the ref. point in radians - - v3i_yang : float - The angle between ideal Y-axis and V3 in radians. - - vparity : int - The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. - Value should be "1" or "-1". - - Returns - ------- - matrix: [pc1_1, pc1_2, pc2_1, pc2_2] - The rotation matrix - - Notes - ----- - The rotation is - - ---------------- - | pc1_1 pc2_1 | - | pc1_2 pc2_2 | - ---------------- - - """ - if vparity not in (1, -1): - raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - - rel_angle = roll_ref - (vparity * v3i_yang) - - pc1_1 = vparity * np.cos(rel_angle) - pc1_2 = np.sin(rel_angle) - pc2_1 = vparity * -np.sin(rel_angle) - pc2_2 = np.cos(rel_angle) - - return [pc1_1, pc1_2, pc2_1, pc2_2] - - -def _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes -): - lon, lat = spatial_footprint - lon, lat = np.deg2rad(lon), np.deg2rad(lat) - x = np.cos(lat) * np.cos(lon) - y = np.cos(lat) * np.sin(lon) - z = np.sin(lat) - - x_mid = (np.max(x) + np.min(x)) / 2.0 - y_mid = (np.max(y) + np.min(y)) / 2.0 - z_mid = (np.max(z) + np.min(z)) / 2.0 - lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 - lat_fiducial = np.rad2deg( - np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) - ) - fiducial[spatial_axes] = lon_fiducial, lat_fiducial - - -def compute_fiducial(wcslist, bounding_box=None): - """ - For a celestial footprint this is the center. - For a spectral footprint, it is the beginning of the range. - - This function assumes all WCSs have the same output coordinate frame. - """ - - axes_types = wcslist[0].output_frame.axes_type - spatial_axes = np.array(axes_types) == "SPATIAL" - spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack( - [w.footprint(bounding_box=bounding_box).T for w in wcslist] - ) - spatial_footprint = footprints[spatial_axes] - spectral_footprint = footprints[spectral_axes] - - fiducial = np.empty(len(axes_types)) - if spatial_footprint.any(): - _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes - ) - if spectral_footprint.any(): - fiducial[spectral_axes] = spectral_footprint.min() - return fiducial - - -def wcsinfo_from_model(input_model): - """ - Create a dict {wcs_keyword: array_of_values} pairs from a data model. - - Parameters - ---------- - input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` - The input data model - - """ - defaults = { - "CRPIX": 0, - "CRVAL": 0, - "CDELT": 1.0, - "CTYPE": "", - "CUNIT": u.Unit(""), - } - wcsaxes = input_model.meta.wcsinfo.wcsaxes - wcsinfo = {"WCSAXES": wcsaxes} - for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: - val = [] - for ax in range(1, wcsaxes + 1): - k = (key + "{0}".format(ax)).lower() - v = getattr(input_model.meta.wcsinfo, k, defaults[key]) - val.append(v) - wcsinfo[key] = np.array(val) - - pc = np.zeros((wcsaxes, wcsaxes)) - for i in range(1, wcsaxes + 1): - for j in range(1, wcsaxes + 1): - pc[i - 1, j - 1] = getattr( - input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 - ) - wcsinfo["PC"] = pc - wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame - wcsinfo["has_cd"] = False - return wcsinfo - - -def _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial -): - wcsinfo = wcsinfo_from_model(refmodel) - if isinstance(refmodel, JwstDataModel): - sky_axes, spec, other = gwutils.get_axes(wcsinfo) - elif isinstance(refmodel, DataModel): - sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - - # Need to put the rotation matrix (List[float, float, float, float]) - # returned from calc_rotation_matrix into the correct shape for - # constructing the transformation - v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) - vparity = refmodel.meta.wcsinfo.vparity - if rotation is None: - roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) - else: - roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) - - pc = np.reshape( - calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) - ) - - rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") - transform = [rotation] - if sky_axes: - if not pscale: - pscale = compute_scale( - refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio - ) - transform.append( - astmodels.Scale(pscale, name="cdelt1") - & astmodels.Scale(pscale, name="cdelt2") - ) - - if transform: - transform = functools.reduce(lambda x, y: x | y, transform) - return transform - - -def wcs_from_footprints( - dmodels, - refmodel=None, - transform=None, - bounding_box=None, - pscale_ratio=None, - pscale=None, - rotation=None, - shape=None, - crpix=None, - crval=None, -): - """ - Create a WCS from a list of input data models. - - A fiducial point in the output coordinate frame is created from the - footprints of all WCS objects. For a spatial frame this is the center - of the union of the footprints. For a spectral frame the fiducial is in - the beginning of the footprint range. - If ``refmodel`` is None, the first WCS object in the list is considered - a reference. The output coordinate frame and projection (for celestial frames) - is taken from ``refmodel``. - If ``transform`` is not supplied, a compound transform is created using - CDELTs and PC. - If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed - from bounding_box of all input WCSs. - - Parameters - ---------- - dmodels : list of `~jwst.datamodels.JwstDataModel` - A list of data models. - refmodel : `~jwst.datamodels.JwstDataModel`, optional - This model's WCS is used as a reference. - WCS. The output coordinate frame, the projection and a - scaling and rotation transform is created from it. If not supplied - the first model in the list is used as ``refmodel``. - transform : `~astropy.modeling.core.Model`, optional - A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` - If not supplied Scaling | Rotation is computed from ``refmodel``. - bounding_box : tuple, optional - Bounding_box of the new WCS. - If not supplied it is computed from the bounding_box of all inputs. - pscale_ratio : float, None, optional - Ratio of input to output pixel scale. Ignored when either - ``transform`` or ``pscale`` are provided. - pscale : float, None, optional - Absolute pixel scale in degrees. When provided, overrides - ``pscale_ratio``. Ignored when ``transform`` is provided. - rotation : float, None, optional - Position angle of output image's Y-axis relative to North. - A value of 0.0 would orient the final output image to be North up. - The default of `None` specifies that the images will not be rotated, - but will instead be resampled in the default orientation for the camera - with the x and y axes of the resampled image corresponding - approximately to the detector axes. Ignored when ``transform`` is - provided. - shape : tuple of int, None, optional - Shape of the image (data array) using ``numpy.ndarray`` convention - (``ny`` first and ``nx`` second). This value will be assigned to - ``pixel_shape`` and ``array_shape`` properties of the returned - WCS object. - crpix : tuple of float, None, optional - Position of the reference pixel in the image array. If ``crpix`` is not - specified, it will be set to the center of the bounding box of the - returned WCS object. - crval : tuple of float, None, optional - Right ascension and declination of the reference pixel. Automatically - computed if not provided. - - """ - bb = bounding_box - wcslist = [im.meta.wcs for im in dmodels] - - if not isiterable(wcslist): - raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") - - if not all(isinstance(w, WCS) for w in wcslist): - raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") - - if refmodel is None: - refmodel = dmodels[0] - elif not isinstance(refmodel, (JwstDataModel, DataModel)): - raise TypeError("Expected refmodel to be an instance of DataModel.") - - fiducial = compute_fiducial(wcslist, bb) - if crval is not None: - # overwrite spatial axes with user-provided CRVAL: - i = 0 - for k, axt in enumerate(wcslist[0].output_frame.axes_type): - if axt == "SPATIAL": - fiducial[k] = crval[i] - i += 1 - - ref_fiducial = np.array( - [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] - ) - - prj = astmodels.Pix2Sky_TAN() - - if transform is None: - transform = _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial - ) - - out_frame = refmodel.meta.wcs.output_frame - input_frame = refmodel.meta.wcs.input_frame - wnew = wcs_from_fiducial( - fiducial, - coordinate_frame=out_frame, - projection=prj, - transform=transform, - input_frame=input_frame, - ) - - footprints = [w.footprint().T for w in wcslist] - domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) - axis_min_values = np.min(domain_bounds, axis=1) - domain_bounds = (domain_bounds.T - axis_min_values).T - - output_bounding_box = [] - for axis in out_frame.axes_order: - axis_min, axis_max = ( - domain_bounds[axis].min(), - domain_bounds[axis].max(), - ) - output_bounding_box.append((axis_min, axis_max)) - - output_bounding_box = tuple(output_bounding_box) - if crpix is None: - offset1, offset2 = wnew.backward_transform(*fiducial) - offset1 -= axis_min_values[0] - offset2 -= axis_min_values[1] - else: - offset1, offset2 = crpix - offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" - ) - - wnew.insert_transform("detector", offsets, after=True) - wnew.bounding_box = output_bounding_box - - if shape is None: - shape = [ - int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] - ] - - wnew.pixel_shape = shape[::-1] - wnew.array_shape = shape - - return wnew From 9ab2e44e990619197fcf327bf318b06dea5977f9 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 6 Jul 2023 15:16:47 -0400 Subject: [PATCH 007/117] Update .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 51a50d9b..c4fc59e8 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ coverage.xml .pytest_cache/ cover/ coverage/ +cov.xml # Translations *.mo From 9441dd496d841a2c31d7d33374c4d522a872c98d Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 6 Jul 2023 16:49:24 -0400 Subject: [PATCH 008/117] Add methods necessary for resample. --- src/stcal/alignment/util.py | 420 ++++++++++++++++++++++++++++++++++++ tests/test_alignment.py | 144 +++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 tests/test_alignment.py diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index e69de29b..53f040b9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -0,0 +1,420 @@ +""" +Utility function for assign_wcs. + +""" +import logging +import functools +import numpy as np + +from astropy.coordinates import SkyCoord +from astropy.utils.misc import isiterable +from astropy import units as u +from astropy.modeling import models as astmodels +from typing import Union, List + +from gwcs import WCS +from gwcs import utils as gwutils +from gwcs.wcstools import wcs_from_fiducial + +from stdatamodels.jwst.datamodels import JwstDataModel +from roman_datamodels.datamodels import DataModel + + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +_MAX_SIP_DEGREE = 6 + + +__all__ = [ + "wcs_from_footprints", + "compute_scale", + "calc_rotation_matrix", +] + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : `~gwcs.wcs.WCS` + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def calc_rotation_matrix( + roll_ref: float, v3i_yang: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yang : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: [pc1_1, pc1_2, pc2_1, pc2_2] + The rotation matrix + + Notes + ----- + The rotation is + + ---------------- + | pc1_1 pc2_1 | + | pc1_2 pc2_2 | + ---------------- + + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yang) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + +def _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes +): + lon, lat = spatial_footprint + lon, lat = np.deg2rad(lon), np.deg2rad(lat) + x = np.cos(lat) * np.cos(lon) + y = np.cos(lat) * np.sin(lon) + z = np.sin(lat) + + x_mid = (np.max(x) + np.min(x)) / 2.0 + y_mid = (np.max(y) + np.min(y)) / 2.0 + z_mid = (np.max(z) + np.min(z)) / 2.0 + lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 + lat_fiducial = np.rad2deg( + np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) + ) + fiducial[spatial_axes] = lon_fiducial, lat_fiducial + + +def compute_fiducial(wcslist, bounding_box=None): + """ + For a celestial footprint this is the center. + For a spectral footprint, it is the beginning of the range. + + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def wcsinfo_from_model(input_model): + """ + Create a dict {wcs_keyword: array_of_values} pairs from a data model. + + Parameters + ---------- + input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` + The input data model + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes)) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial +): + wcsinfo = wcsinfo_from_model(refmodel) + if isinstance(refmodel, JwstDataModel): + sky_axes, spec, other = gwutils.get_axes(wcsinfo) + elif isinstance(refmodel, DataModel): + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + + # Need to put the rotation matrix (List[float, float, float, float]) + # returned from calc_rotation_matrix into the correct shape for + # constructing the transformation + v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) + vparity = refmodel.meta.wcsinfo.vparity + if rotation is None: + roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + else: + roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + + pc = np.reshape( + calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + ) + + rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") + transform = [rotation] + if sky_axes: + if not pscale: + pscale = compute_scale( + refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio + ) + transform.append( + astmodels.Scale(pscale, name="cdelt1") + & astmodels.Scale(pscale, name="cdelt2") + ) + + if transform: + transform = functools.reduce(lambda x, y: x | y, transform) + return transform + + +def wcs_from_footprints( + dmodels, + refmodel=None, + transform=None, + bounding_box=None, + pscale_ratio=None, + pscale=None, + rotation=None, + shape=None, + crpix=None, + crval=None, +): + """ + Create a WCS from a list of input data models. + + A fiducial point in the output coordinate frame is created from the + footprints of all WCS objects. For a spatial frame this is the center + of the union of the footprints. For a spectral frame the fiducial is in + the beginning of the footprint range. + If ``refmodel`` is None, the first WCS object in the list is considered + a reference. The output coordinate frame and projection (for celestial frames) + is taken from ``refmodel``. + If ``transform`` is not supplied, a compound transform is created using + CDELTs and PC. + If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed + from bounding_box of all input WCSs. + + Parameters + ---------- + dmodels : list of `~jwst.datamodels.JwstDataModel` + A list of data models. + refmodel : `~jwst.datamodels.JwstDataModel`, optional + This model's WCS is used as a reference. + WCS. The output coordinate frame, the projection and a + scaling and rotation transform is created from it. If not supplied + the first model in the list is used as ``refmodel``. + transform : `~astropy.modeling.core.Model`, optional + A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` + If not supplied Scaling | Rotation is computed from ``refmodel``. + bounding_box : tuple, optional + Bounding_box of the new WCS. + If not supplied it is computed from the bounding_box of all inputs. + pscale_ratio : float, None, optional + Ratio of input to output pixel scale. Ignored when either + ``transform`` or ``pscale`` are provided. + pscale : float, None, optional + Absolute pixel scale in degrees. When provided, overrides + ``pscale_ratio``. Ignored when ``transform`` is provided. + rotation : float, None, optional + Position angle of output image's Y-axis relative to North. + A value of 0.0 would orient the final output image to be North up. + The default of `None` specifies that the images will not be rotated, + but will instead be resampled in the default orientation for the camera + with the x and y axes of the resampled image corresponding + approximately to the detector axes. Ignored when ``transform`` is + provided. + shape : tuple of int, None, optional + Shape of the image (data array) using ``numpy.ndarray`` convention + (``ny`` first and ``nx`` second). This value will be assigned to + ``pixel_shape`` and ``array_shape`` properties of the returned + WCS object. + crpix : tuple of float, None, optional + Position of the reference pixel in the image array. If ``crpix`` is not + specified, it will be set to the center of the bounding box of the + returned WCS object. + crval : tuple of float, None, optional + Right ascension and declination of the reference pixel. Automatically + computed if not provided. + + """ + bb = bounding_box + wcslist = [im.meta.wcs for im in dmodels] + + if not isiterable(wcslist): + raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") + + if not all(isinstance(w, WCS) for w in wcslist): + raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") + + if refmodel is None: + refmodel = dmodels[0] + elif not isinstance(refmodel, (JwstDataModel, DataModel)): + raise TypeError("Expected refmodel to be an instance of DataModel.") + + fiducial = compute_fiducial(wcslist, bb) + if crval is not None: + # overwrite spatial axes with user-provided CRVAL: + i = 0 + for k, axt in enumerate(wcslist[0].output_frame.axes_type): + if axt == "SPATIAL": + fiducial[k] = crval[i] + i += 1 + + ref_fiducial = np.array( + [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + ) + + prj = astmodels.Pix2Sky_TAN() + + if transform is None: + transform = _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial + ) + + out_frame = refmodel.meta.wcs.output_frame + input_frame = refmodel.meta.wcs.input_frame + wnew = wcs_from_fiducial( + fiducial, + coordinate_frame=out_frame, + projection=prj, + transform=transform, + input_frame=input_frame, + ) + + footprints = [w.footprint().T for w in wcslist] + domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in out_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + if crpix is None: + offset1, offset2 = wnew.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] + else: + offset1, offset2 = crpix + offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" + ) + + wnew.insert_transform("detector", offsets, after=True) + wnew.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wnew.pixel_shape = shape[::-1] + wnew.array_shape = shape + + return wnew diff --git a/tests/test_alignment.py b/tests/test_alignment.py new file mode 100644 index 00000000..8e5aac4b --- /dev/null +++ b/tests/test_alignment.py @@ -0,0 +1,144 @@ +from astropy.modeling import models +from astropy import coordinates as coord +from astropy import units as u +from gwcs import WCS +from gwcs import coordinate_frames as cf +import numpy as np +import pytest +from stdatamodels.jwst.datamodels import ImageModel +from roman_datamodels.datamodels import DataModel +from stcal.alignment.util import ( + compute_fiducial, + compute_scale, + wcs_from_footprints, +) + + +def _create_wcs_object_without_distortion( + fiducial_world=(None, None), + pscale=None, + shape=None, +): + fiducial_detector = tuple(shape.value) + + # subtract 1 to account for pixel indexing starting at 0 + shift = models.Shift(-(fiducial_detector[0] - 1)) & models.Shift( + -(fiducial_detector[1] - 1) + ) + + scale = models.Scale(pscale[0].to("deg")) & models.Scale( + pscale[1].to("deg") + ) + + tan = models.Pix2Sky_TAN() + celestial_rotation = models.RotateNative2Celestial( + fiducial_world[0], + fiducial_world[1], + 180 * u.deg, + ) + + det2sky = shift | scale | tan | celestial_rotation + det2sky.name = "linear_transform" + + detector_frame = cf.Frame2D( + name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix) + ) + sky_frame = cf.CelestialFrame( + reference_frame=coord.FK5(), name="fk5", unit=(u.deg, u.deg) + ) + + pipeline = [(detector_frame, det2sky), (sky_frame, None)] + + wcs_obj = WCS(pipeline) + + wcs_obj.bounding_box = ( + (-0.5, fiducial_detector[0] - 0.5), + (-0.5, fiducial_detector[0] - 0.5), + ) + + return wcs_obj + + +def _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale): + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + datamodel = ImageModel(np.zeros(tuple(shape.value.astype(int)))) + datamodel.meta.wcs = wcs + datamodel.meta.wcsinfo.ra_ref = fiducial_world[0].value + datamodel.meta.wcsinfo.dec_ref = fiducial_world[1].value + datamodel.meta.wcsinfo.v2_ref = 0 + datamodel.meta.wcsinfo.v3_ref = 0 + datamodel.meta.wcsinfo.roll_ref = 0 + datamodel.meta.wcsinfo.v3yangle = 0 + datamodel.meta.wcsinfo.vparity = -1 + datamodel.meta.wcsinfo.wcsaxes = 2 + datamodel.meta.coordinates.reference_frame = "ICRS" + datamodel.meta.wcsinfo.ctype1 = "RA---TAN" + datamodel.meta.wcsinfo.ctype2 = "DEC--TAN" + + return (wcs, datamodel) + + +def test_compute_fiducial(): + """Test that util.compute_fiducial can properly determine the center of the + WCS's footprint. + """ + + shape = (3, 3) * u.pix + fiducial_world = (0, 0) * u.deg + pscale = (0.05, 0.05) * u.arcsec + + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + + computed_fiducial = compute_fiducial([wcs]) + + assert all(np.isclose(wcs(1, 1), computed_fiducial)) + + +@pytest.mark.parametrize("pscales", [(0.05, 0.05), (0.1, 0.05)]) +def test_compute_scale(pscales): + """Test that util.compute_scale can properly determine the pixel scale of a + WCS object. + """ + shape = (3, 3) * u.pix + fiducial_world = (0, 0) * u.deg + pscale = (pscales[0], pscales[1]) * u.arcsec + + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + expected_scale = np.sqrt(pscale[0].to("deg") * pscale[1].to("deg")).value + + computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world.value) + + assert np.isclose(expected_scale, computed_scale) + + +def test_wcs_from_footprints(): + shape = (3, 3) * u.pix + fiducial_world = (10, 0) * u.deg + pscale = (0.1, 0.1) * u.arcsec + + wcs_1, dm_1 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + + # new fiducial will be shifted by one pixel in both directions + fiducial_world -= pscale + wcs_2, dm_2 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + + # check overlapping pixels have approximate the same world coordinate + assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) + assert all(np.isclose(wcs_1(1, 0), wcs_2(2, 1))) + assert all(np.isclose(wcs_1(0, 0), wcs_2(1, 1))) + assert all(np.isclose(wcs_1(1, 1), wcs_2(2, 2))) + + wcs = wcs_from_footprints([dm_1, dm_2]) + + # check that center of calculated WCS matches the + # expected position onto wcs_1 and wcs_2 + assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) + assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + + assert True From f0013aad0f79da8eb3524c05154165976d048050 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:01:21 -0400 Subject: [PATCH 009/117] Small changes to accommodate both JWST and RST datamodels. --- src/stcal/alignment/util.py | 9 ++-- tests/test_alignment.py | 82 +++++++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 53f040b9..063db199 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -17,7 +17,7 @@ from gwcs.wcstools import wcs_from_fiducial from stdatamodels.jwst.datamodels import JwstDataModel -from roman_datamodels.datamodels import DataModel +from roman_datamodels.datamodels import DataModel as RstDataModel log = logging.getLogger(__name__) @@ -231,10 +231,11 @@ def wcsinfo_from_model(input_model): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - wcsinfo = wcsinfo_from_model(refmodel) if isinstance(refmodel, JwstDataModel): + wcsinfo = wcsinfo_from_model(refmodel) sky_axes, spec, other = gwutils.get_axes(wcsinfo) - elif isinstance(refmodel, DataModel): + elif isinstance(refmodel, RstDataModel): + wcsinfo = refmodel.meta.wcsinfo sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -349,7 +350,7 @@ def wcs_from_footprints( if refmodel is None: refmodel = dmodels[0] - elif not isinstance(refmodel, (JwstDataModel, DataModel)): + elif not isinstance(refmodel, (JwstDataModel, RstDataModel)): raise TypeError("Expected refmodel to be an instance of DataModel.") fiducial = compute_fiducial(wcslist, bb) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 8e5aac4b..3c2bb3fe 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -6,7 +6,8 @@ import numpy as np import pytest from stdatamodels.jwst.datamodels import ImageModel -from roman_datamodels.datamodels import DataModel +from roman_datamodels import datamodels as rdm +from roman_datamodels import maker_utils as utils from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -59,25 +60,57 @@ def _create_wcs_object_without_distortion( return wcs_obj -def _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale): +def _create_wcs_and_datamodel(datamodel_type, fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - datamodel = ImageModel(np.zeros(tuple(shape.value.astype(int)))) - datamodel.meta.wcs = wcs - datamodel.meta.wcsinfo.ra_ref = fiducial_world[0].value - datamodel.meta.wcsinfo.dec_ref = fiducial_world[1].value - datamodel.meta.wcsinfo.v2_ref = 0 - datamodel.meta.wcsinfo.v3_ref = 0 - datamodel.meta.wcsinfo.roll_ref = 0 - datamodel.meta.wcsinfo.v3yangle = 0 - datamodel.meta.wcsinfo.vparity = -1 - datamodel.meta.wcsinfo.wcsaxes = 2 - datamodel.meta.coordinates.reference_frame = "ICRS" - datamodel.meta.wcsinfo.ctype1 = "RA---TAN" - datamodel.meta.wcsinfo.ctype2 = "DEC--TAN" - - return (wcs, datamodel) + if datamodel_type == "jwst": + datamodel = _create_jwst_meta(shape, fiducial_world, wcs) + elif datamodel_type == "roman": + datamodel = _create_roman_meta(shape, fiducial_world, wcs) + + return datamodel + + +def _create_jwst_meta(shape, fiducial_world, wcs): + result = ImageModel(np.zeros(tuple(shape.value.astype(int)))) + + result.meta.wcsinfo.ra_ref = fiducial_world[0].value + result.meta.wcsinfo.dec_ref = fiducial_world[1].value + result.meta.wcsinfo.ctype1 = "RA---TAN" + result.meta.wcsinfo.ctype2 = "DEC--TAN" + result.meta.wcsinfo.v2_ref = 0 + result.meta.wcsinfo.v3_ref = 0 + result.meta.wcsinfo.roll_ref = 0 + result.meta.wcsinfo.v3yangle = 0 + result.meta.wcsinfo.vparity = -1 + result.meta.wcsinfo.wcsaxes = 2 + + result.meta.coordinates.reference_frame = "ICRS" + + result.meta.wcs = wcs + + return result + + +def _create_roman_meta(shape, fiducial_world, wcs): + result = utils.mk_level2_image(shape=tuple(shape.value.astype(int))) + + result.meta.wcsinfo.ra_ref = fiducial_world[0].value + result.meta.wcsinfo.dec_ref = fiducial_world[1].value + result.meta.wcsinfo.v2_ref = 0 + result.meta.wcsinfo.v3_ref = 0 + result.meta.wcsinfo.roll_ref = 0 + result.meta.wcsinfo.v3yangle = 0 + result.meta.wcsinfo.vparity = -1 + + result.meta.coordinates.reference_frame = "ICRS" + + result.meta["wcs"] = wcs + + result = rdm.ImageModel(result) + + return result def test_compute_fiducial(): @@ -117,16 +150,23 @@ def test_compute_scale(pscales): assert np.isclose(expected_scale, computed_scale) -def test_wcs_from_footprints(): +@pytest.mark.parametrize("datamodel_type", ["jwst", "roman"]) +def test_wcs_from_footprints(datamodel_type): shape = (3, 3) * u.pix fiducial_world = (10, 0) * u.deg pscale = (0.1, 0.1) * u.arcsec - wcs_1, dm_1 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + dm_1 = _create_wcs_and_datamodel( + datamodel_type, fiducial_world, shape, pscale + ) + wcs_1 = dm_1.meta.wcs # new fiducial will be shifted by one pixel in both directions fiducial_world -= pscale - wcs_2, dm_2 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + dm_2 = _create_wcs_and_datamodel( + datamodel_type, fiducial_world, shape, pscale + ) + wcs_2 = dm_2.meta.wcs # check overlapping pixels have approximate the same world coordinate assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) @@ -140,5 +180,3 @@ def test_wcs_from_footprints(): # expected position onto wcs_1 and wcs_2 assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) - - assert True From 66ea089c3c16d6a3f20b7e133d022affbec27683 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:07:27 -0400 Subject: [PATCH 010/117] Add CHANGES.rst entry. --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8b8d5d07..d239c3fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +1.4.2 (unreleased) +================== +- Added ``alignment`` sub-package. + 1.4.1 (2023-06-29) ================== From 48e3d6096a21c1f2bd927454f7512cd7f660bb02 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:08:39 -0400 Subject: [PATCH 011/117] Add PR number to CHANGES.rst entry. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d239c3fa..030bd1f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,6 @@ 1.4.2 (unreleased) ================== -- Added ``alignment`` sub-package. +- Added ``alignment`` sub-package. [#179] 1.4.1 (2023-06-29) ================== From 66ba3fecf4ee29b1e449a428b5fc75ca63f07a07 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:50:16 -0400 Subject: [PATCH 012/117] use protocols --- src/stcal/alignment/util.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 063db199..3b074bde 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -4,21 +4,20 @@ """ import logging import functools +from typing import List, Protocol, Union + import numpy as np from astropy.coordinates import SkyCoord from astropy.utils.misc import isiterable from astropy import units as u from astropy.modeling import models as astmodels -from typing import Union, List +from asdf import AsdfFile from gwcs import WCS from gwcs import utils as gwutils from gwcs.wcstools import wcs_from_fiducial -from stdatamodels.jwst.datamodels import JwstDataModel -from roman_datamodels.datamodels import DataModel as RstDataModel - log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -34,6 +33,13 @@ ] +class SupportsDataWithWcs(Protocol): + _asdf: AsdfFile + + def to_flat_dict(): + ... + + def compute_scale( wcs: WCS, fiducial: Union[tuple, np.ndarray], @@ -189,7 +195,7 @@ def compute_fiducial(wcslist, bounding_box=None): return fiducial -def wcsinfo_from_model(input_model): +def wcsinfo_from_model(input_model: SupportsDataWithWcs): """ Create a dict {wcs_keyword: array_of_values} pairs from a data model. @@ -231,16 +237,12 @@ def wcsinfo_from_model(input_model): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - if isinstance(refmodel, JwstDataModel): - wcsinfo = wcsinfo_from_model(refmodel) - sky_axes, spec, other = gwutils.get_axes(wcsinfo) - elif isinstance(refmodel, RstDataModel): - wcsinfo = refmodel.meta.wcsinfo - sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - - # Need to put the rotation matrix (List[float, float, float, float]) - # returned from calc_rotation_matrix into the correct shape for - # constructing the transformation + wcsinfo = refmodel.meta.wcsinfo + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + + # Need to put the rotation matrix (List[float, float, float, float]) + # returned from calc_rotation_matrix into the correct shape for + # constructing the transformation v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) vparity = refmodel.meta.wcsinfo.vparity if rotation is None: From 3906abfabfee10ce40f4766d2e6e1cf9bdab6b44 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:52:03 -0400 Subject: [PATCH 013/117] temp --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddf0a206..3d4bd018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main - '*x' + - '*' tags: - '*' pull_request: From 3467f818796364c1288ca0ea248b1f6aa7df965e Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:58:28 -0400 Subject: [PATCH 014/117] add dependencies --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 58c16764..b5656be0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,10 @@ dependencies = [ 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', + 'asdf', + 'gwcs', + 'stdatamodels', + 'roman_datamodels', ] dynamic = ['version'] From 13c342f31b4c2691f587f85316665bdada1111aa Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 17:49:23 -0400 Subject: [PATCH 015/117] add CI testing to alignment branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d4bd018..3657a391 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,12 @@ on: branches: - main - '*x' - - '*' tags: - '*' pull_request: branches: - main + - stcal-alignment schedule: # Weekly Monday 9AM build - cron: "0 9 * * 1" From 42423d3a97ad155f4fde3f60b31e82c6b2b0612c Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 17:51:08 -0400 Subject: [PATCH 016/117] add CI testing to alignment branch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3657a391..4d8a8e8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main - '*x' + - use-protocols tags: - '*' pull_request: From b8b8cc035dcb82b7c2ef5cb4a63111e0ae2610f1 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 20:08:15 -0400 Subject: [PATCH 017/117] fix test --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 - src/stcal/alignment/util.py | 4 -- tests/test_alignment.py | 92 +++++++++++++++---------------------- 4 files changed, 39 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d8a8e8a..c7e9e8f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - main - '*x' - - use-protocols + - stcal-alignment tags: - '*' pull_request: diff --git a/pyproject.toml b/pyproject.toml index b5656be0..9522b45c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,6 @@ dependencies = [ 'opencv-python-headless >=4.6.0.66', 'asdf', 'gwcs', - 'stdatamodels', - 'roman_datamodels', ] dynamic = ['version'] diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 3b074bde..4464bed9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -15,7 +15,6 @@ from asdf import AsdfFile from gwcs import WCS -from gwcs import utils as gwutils from gwcs.wcstools import wcs_from_fiducial @@ -237,7 +236,6 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - wcsinfo = refmodel.meta.wcsinfo sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -352,8 +350,6 @@ def wcs_from_footprints( if refmodel is None: refmodel = dmodels[0] - elif not isinstance(refmodel, (JwstDataModel, RstDataModel)): - raise TypeError("Expected refmodel to be an instance of DataModel.") fiducial = compute_fiducial(wcslist, bb) if crval is not None: diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 3c2bb3fe..77d8a544 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -1,13 +1,14 @@ +import numpy as np + from astropy.modeling import models from astropy import coordinates as coord from astropy import units as u + from gwcs import WCS from gwcs import coordinate_frames as cf -import numpy as np + import pytest -from stdatamodels.jwst.datamodels import ImageModel -from roman_datamodels import datamodels as rdm -from roman_datamodels import maker_utils as utils + from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -16,9 +17,9 @@ def _create_wcs_object_without_distortion( - fiducial_world=(None, None), - pscale=None, - shape=None, + fiducial_world, + pscale, + shape, ): fiducial_detector = tuple(shape.value) @@ -60,57 +61,45 @@ def _create_wcs_object_without_distortion( return wcs_obj -def _create_wcs_and_datamodel(datamodel_type, fiducial_world, shape, pscale): +def _create_wcs_and_datamodel(fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - if datamodel_type == "jwst": - datamodel = _create_jwst_meta(shape, fiducial_world, wcs) - elif datamodel_type == "roman": - datamodel = _create_roman_meta(shape, fiducial_world, wcs) - + ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value + datamodel = DataModel(ra_ref=ra_ref, dec_ref=dec_ref, roll_ref=0, + v2_ref=0, v3_ref=0, v3yangle=0, wcs=wcs) return datamodel -def _create_jwst_meta(shape, fiducial_world, wcs): - result = ImageModel(np.zeros(tuple(shape.value.astype(int)))) +class WcsInfo: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle): + self.ra_ref = ra_ref + self.dec_ref = dec_ref + self.ctype1 = "RA---TAN" + self.ctype2 = "DEC--TAN" + self.v2_ref = v2_ref + self.v3_ref = v3_ref + self.v3yangle = v3yangle + self.roll_ref = roll_ref + self.vparity = -1 + self.wcsaxes = 2 - result.meta.wcsinfo.ra_ref = fiducial_world[0].value - result.meta.wcsinfo.dec_ref = fiducial_world[1].value - result.meta.wcsinfo.ctype1 = "RA---TAN" - result.meta.wcsinfo.ctype2 = "DEC--TAN" - result.meta.wcsinfo.v2_ref = 0 - result.meta.wcsinfo.v3_ref = 0 - result.meta.wcsinfo.roll_ref = 0 - result.meta.wcsinfo.v3yangle = 0 - result.meta.wcsinfo.vparity = -1 - result.meta.wcsinfo.wcsaxes = 2 - result.meta.coordinates.reference_frame = "ICRS" +class Coordinates: + def __init__(self): + self.reference_frame = "ICRS" - result.meta.wcs = wcs - return result +class MetaData: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): + self.wcsinfo = WcsInfo(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle) + self.wcs = wcs + self.coordinates=Coordinates() -def _create_roman_meta(shape, fiducial_world, wcs): - result = utils.mk_level2_image(shape=tuple(shape.value.astype(int))) - - result.meta.wcsinfo.ra_ref = fiducial_world[0].value - result.meta.wcsinfo.dec_ref = fiducial_world[1].value - result.meta.wcsinfo.v2_ref = 0 - result.meta.wcsinfo.v3_ref = 0 - result.meta.wcsinfo.roll_ref = 0 - result.meta.wcsinfo.v3yangle = 0 - result.meta.wcsinfo.vparity = -1 - - result.meta.coordinates.reference_frame = "ICRS" - - result.meta["wcs"] = wcs - - result = rdm.ImageModel(result) - - return result +class DataModel: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): + self.meta = MetaData(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs) def test_compute_fiducial(): @@ -150,22 +139,17 @@ def test_compute_scale(pscales): assert np.isclose(expected_scale, computed_scale) -@pytest.mark.parametrize("datamodel_type", ["jwst", "roman"]) -def test_wcs_from_footprints(datamodel_type): +def test_wcs_from_footprints(): shape = (3, 3) * u.pix fiducial_world = (10, 0) * u.deg pscale = (0.1, 0.1) * u.arcsec - dm_1 = _create_wcs_and_datamodel( - datamodel_type, fiducial_world, shape, pscale - ) + dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs # new fiducial will be shifted by one pixel in both directions fiducial_world -= pscale - dm_2 = _create_wcs_and_datamodel( - datamodel_type, fiducial_world, shape, pscale - ) + dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs # check overlapping pixels have approximate the same world coordinate From fa86a3e7a77b3f342983028c6f6e6f6938041725 Mon Sep 17 00:00:00 2001 From: mwregan2 Date: Fri, 7 Jul 2023 14:21:00 -0400 Subject: [PATCH 018/117] Add setting of number_extended_events (#178) * Add setting of number_extended_events This was not being set when in single processing mode. * Update CHANGES.rst * Update CHANGES.rst * Update test_jump.py --------- Co-authored-by: Howard Bushouse --- CHANGES.rst | 11 +++++++++++ src/stcal/jump/jump.py | 6 +++++- tests/test_jump.py | 14 ++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 030bd1f9..c72120d9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,17 @@ Bug Fixes jump ~~~~ +- Added setting of number_extended_events for non-multiprocessing + mode. This is the value that is put into the header keyword EXTNCRS. [#178] + +1.4.1 (2023-06-29) + +Bug Fixes +--------- + +jump +~~~~ + - Added statement to prevent the number of cores used in multiprocessing from being larger than the number of rows. This was causing some CI tests to fail. [#176] diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 250d1ebe..078d58b0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -262,13 +262,15 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, only_use_ints=only_use_ints) # This is the flag that controls the flagging of either snowballs. if expand_large_events: - flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, + total_snowballs = flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, min_jump_area=min_jump_area, expand_factor=expand_factor, sat_required_snowball=sat_required_snowball, min_sat_radius_extend=min_sat_radius_extend, edge_size=edge_size, sat_expand=sat_expand, max_extended_radius=max_extended_radius) + log.info('Total snowballs = %i' % total_snowballs) + number_extended_events = total_snowballs if find_showers: gdq, num_showers = find_faint_extended(data, gdq, readnoise_2d, frames_per_group, minimum_sigclip_groups, @@ -280,6 +282,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, ellipse_expand=extend_ellipse_expand_ratio, num_grps_masked=grps_masked_after_shower, max_extended_radius=max_extended_radius) + log.info('Total showers= %i' % num_showers) + number_extended_events = num_showers else: yinc = int(n_rows / n_slices) slices = [] diff --git a/tests/test_jump.py b/tests/test_jump.py index ddfcbed1..49d65ba0 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -162,6 +162,7 @@ def test_find_faint_extended(): ellipse_expand=1.1, num_grps_masked=3) # Check that all the expected samples in group 2 are flagged as jump and # that they are not flagged outside + assert (num_showers == 3) assert (np.all(gdq[0, 1, 22, 14:23] == 0)) assert (np.all(gdq[0, 1, 21, 16:20] == DQFLAGS['JUMP_DET'])) assert (np.all(gdq[0, 1, 20, 15:22] == DQFLAGS['JUMP_DET'])) @@ -210,6 +211,7 @@ def test_find_faint_extended_sigclip(): ellipse_expand=1.1, num_grps_masked=3) # Check that all the expected samples in group 2 are flagged as jump and # that they are not flagged outside + assert(num_showers == 0) assert (np.all(gdq[0, 1, 22, 14:23] == 0)) assert (np.all(gdq[0, 1, 21, 16:20] == 0)) assert (np.all(gdq[0, 1, 20, 15:22] == 0)) @@ -265,10 +267,14 @@ def test_inputjumpall(): @pytest.mark.skip("Used for local testing") def test_inputjump_sat_star(): testcube = fits.getdata('data/input_gdq_flarge.fits') - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + num_extended_events = flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], + min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, + sat_required_snowball=True, + min_sat_radius_extend=2.5, + sat_expand=2) + assert(num_extended_events == 312) fits.writeto("outgdq2.fits", testcube, overwrite=True) From 0eea0e5d4902286dd7b1966537dc648ef3f14cbf Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Mon, 10 Jul 2023 11:18:06 -0400 Subject: [PATCH 019/117] Rename variables with FITS keywords names. --- src/stcal/alignment/util.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4464bed9..9e24b751 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -278,8 +278,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - crpix=None, - crval=None, + ref_pixel=None, + ref_coord=None, ): """ Create a WCS from a list of input data models. @@ -330,11 +330,11 @@ def wcs_from_footprints( (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - crpix : tuple of float, None, optional - Position of the reference pixel in the image array. If ``crpix`` is not + ref_pixel : tuple of float, None, optional + Position of the reference pixel in the image array. If ``ref_pixel`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - crval : tuple of float, None, optional + ref_coord : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. @@ -352,12 +352,12 @@ def wcs_from_footprints( refmodel = dmodels[0] fiducial = compute_fiducial(wcslist, bb) - if crval is not None: + if ref_coord is not None: # overwrite spatial axes with user-provided CRVAL: i = 0 for k, axt in enumerate(wcslist[0].output_frame.axes_type): if axt == "SPATIAL": - fiducial[k] = crval[i] + fiducial[k] = ref_coord[i] i += 1 ref_fiducial = np.array( @@ -395,14 +395,14 @@ def wcs_from_footprints( output_bounding_box.append((axis_min, axis_max)) output_bounding_box = tuple(output_bounding_box) - if crpix is None: + if ref_pixel is None: offset1, offset2 = wnew.backward_transform(*fiducial) offset1 -= axis_min_values[0] offset2 -= axis_min_values[1] else: - offset1, offset2 = crpix - offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" + offset1, offset2 = ref_pixel + offsets = astmodels.Shift(-offset1, name="ref_pixel1") & astmodels.Shift( + -offset2, name="ref_pixel2" ) wnew.insert_transform("detector", offsets, after=True) From 7c136a7f3e7eff75343901aada28c01155275d26 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Mon, 10 Jul 2023 11:21:54 -0400 Subject: [PATCH 020/117] Style reformating. --- tests/test_alignment.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 77d8a544..ce4c6863 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -66,9 +66,15 @@ def _create_wcs_and_datamodel(fiducial_world, shape, pscale): fiducial_world=fiducial_world, shape=shape, pscale=pscale ) ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value - datamodel = DataModel(ra_ref=ra_ref, dec_ref=dec_ref, roll_ref=0, - v2_ref=0, v3_ref=0, v3yangle=0, wcs=wcs) - return datamodel + return DataModel( + ra_ref=ra_ref, + dec_ref=dec_ref, + roll_ref=0, + v2_ref=0, + v3_ref=0, + v3yangle=0, + wcs=wcs, + ) class WcsInfo: @@ -91,15 +97,23 @@ def __init__(self): class MetaData: - def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): - self.wcsinfo = WcsInfo(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle) + def __init__( + self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None + ): + self.wcsinfo = WcsInfo( + ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle + ) self.wcs = wcs - self.coordinates=Coordinates() + self.coordinates = Coordinates() class DataModel: - def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): - self.meta = MetaData(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs) + def __init__( + self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None + ): + self.meta = MetaData( + ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs + ) def test_compute_fiducial(): From cc7d689d1af79a730572df019994eb9e8ec68284 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:34:52 -0400 Subject: [PATCH 021/117] Update docs to include stcal.alignment. --- docs/api.rst | 2 +- docs/conf.py | 18 +++++++++++++----- docs/stcal/alignment/description.rst | 4 ++++ docs/stcal/alignment/index.rst | 12 ++++++++++++ docs/stcal/package_index.rst | 1 + 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 docs/stcal/alignment/description.rst create mode 100644 docs/stcal/alignment/index.rst diff --git a/docs/api.rst b/docs/api.rst index 95659c47..a23b1894 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,4 +1,4 @@ -stcall API +stcal API ========== .. automodapi:: stcal diff --git a/docs/conf.py b/docs/conf.py index fbadfd5e..d0fe5754 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,11 +4,12 @@ from pathlib import Path import stsci_rtd_theme + if sys.version_info < (3, 11): import tomli as tomllib else: import tomllib - + def setup(app): try: @@ -27,7 +28,7 @@ def setup(app): # values here: with open(REPO_ROOT / "pyproject.toml", "rb") as configuration_file: conf = tomllib.load(configuration_file) -setup_metadata = conf['project'] +setup_metadata = conf["project"] project = setup_metadata["name"] primary_author = setup_metadata["authors"][0] @@ -38,8 +39,17 @@ def setup(app): version = package.__version__.split("-", 1)[0] release = package.__version__ +# Configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/devdocs", None), + "scipy": ("http://scipy.github.io/devdocs", None), + "matplotlib": ("http://matplotlib.org/", None), +} + extensions = [ "sphinx_automodapi.automodapi", + "sphinx.ext.intersphinx", "numpydoc", ] @@ -48,9 +58,7 @@ def setup(app): autoclass_content = "both" html_theme = "stsci_rtd_theme" -html_theme_options = { - "collapse_navigation": True -} +html_theme_options = {"collapse_navigation": True} html_theme_path = [stsci_rtd_theme.get_html_theme_path()] html_domain_indices = True html_sidebars = {"**": ["globaltoc.html", "relations.html", "searchbox.html"]} diff --git a/docs/stcal/alignment/description.rst b/docs/stcal/alignment/description.rst new file mode 100644 index 00000000..a537e476 --- /dev/null +++ b/docs/stcal/alignment/description.rst @@ -0,0 +1,4 @@ +Description +============ + +This sub-package contains all the modules common to all missions. \ No newline at end of file diff --git a/docs/stcal/alignment/index.rst b/docs/stcal/alignment/index.rst new file mode 100644 index 00000000..e8d65068 --- /dev/null +++ b/docs/stcal/alignment/index.rst @@ -0,0 +1,12 @@ +.. _alignment: + +=============== +Alignment Utils +=============== + +.. toctree:: + :maxdepth: 2 + + description.rst + +.. automodapi:: stcal.alignment diff --git a/docs/stcal/package_index.rst b/docs/stcal/package_index.rst index 44295cd1..b68f11b5 100644 --- a/docs/stcal/package_index.rst +++ b/docs/stcal/package_index.rst @@ -6,3 +6,4 @@ Package Index jump/index.rst ramp_fitting/index.rst + alignment/index.rst \ No newline at end of file From f4f379d391f2772518763e7ec8f815c539b1be1b Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:50:34 -0400 Subject: [PATCH 022/117] Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. --- src/stcal/alignment/util.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 9e24b751..4464bed9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -278,8 +278,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - ref_pixel=None, - ref_coord=None, + crpix=None, + crval=None, ): """ Create a WCS from a list of input data models. @@ -330,11 +330,11 @@ def wcs_from_footprints( (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - ref_pixel : tuple of float, None, optional - Position of the reference pixel in the image array. If ``ref_pixel`` is not + crpix : tuple of float, None, optional + Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - ref_coord : tuple of float, None, optional + crval : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. @@ -352,12 +352,12 @@ def wcs_from_footprints( refmodel = dmodels[0] fiducial = compute_fiducial(wcslist, bb) - if ref_coord is not None: + if crval is not None: # overwrite spatial axes with user-provided CRVAL: i = 0 for k, axt in enumerate(wcslist[0].output_frame.axes_type): if axt == "SPATIAL": - fiducial[k] = ref_coord[i] + fiducial[k] = crval[i] i += 1 ref_fiducial = np.array( @@ -395,14 +395,14 @@ def wcs_from_footprints( output_bounding_box.append((axis_min, axis_max)) output_bounding_box = tuple(output_bounding_box) - if ref_pixel is None: + if crpix is None: offset1, offset2 = wnew.backward_transform(*fiducial) offset1 -= axis_min_values[0] offset2 -= axis_min_values[1] else: - offset1, offset2 = ref_pixel - offsets = astmodels.Shift(-offset1, name="ref_pixel1") & astmodels.Shift( - -offset2, name="ref_pixel2" + offset1, offset2 = crpix + offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" ) wnew.insert_transform("detector", offsets, after=True) From afa9e7ad47d6fb611b14aa07a4609574e0576c18 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:55:12 -0400 Subject: [PATCH 023/117] Updates to address most comments. --- src/stcal/alignment/__init__.py | 1 + src/stcal/alignment/util.py | 140 +++++++++++++++++++++++++------- tests/test_alignment.py | 51 ++++++------ 3 files changed, 137 insertions(+), 55 deletions(-) diff --git a/src/stcal/alignment/__init__.py b/src/stcal/alignment/__init__.py index e69de29b..46d3a156 100644 --- a/src/stcal/alignment/__init__.py +++ b/src/stcal/alignment/__init__.py @@ -0,0 +1 @@ +from .util import * diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4464bed9..134b0e65 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -1,5 +1,5 @@ """ -Utility function for assign_wcs. +Common utility functions for datamodel alignment. """ import logging @@ -22,12 +22,10 @@ log.setLevel(logging.DEBUG) -_MAX_SIP_DEGREE = 6 - - __all__ = [ "wcs_from_footprints", "compute_scale", + "compute_fiducial", "calc_rotation_matrix", ] @@ -53,10 +51,12 @@ def compute_scale( Reference WCS object from which to compute a scaling factor. fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating + reference points. disp_axis : int - Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` + Dispersion axis integer. Assumes the same convention as + `wcsinfo.dispersion_direction` pscale_ratio : int Ratio of input to output pixel scale @@ -104,7 +104,7 @@ def compute_scale( def calc_rotation_matrix( - roll_ref: float, v3i_yang: float, vparity: int = 1 + roll_ref: float, v3i_yangle: float, vparity: int = 1 ) -> List[float]: """Calculate the rotation matrix. @@ -113,7 +113,7 @@ def calc_rotation_matrix( roll_ref : float Telescope roll angle of V3 North over East at the ref. point in radians - v3i_yang : float + v3i_yangle : float The angle between ideal Y-axis and V3 in radians. vparity : int @@ -128,17 +128,14 @@ def calc_rotation_matrix( Notes ----- The rotation is + pc1_1 | pc2_1 - ---------------- - | pc1_1 pc2_1 | - | pc1_2 pc2_2 | - ---------------- - + pc1_2 | pc2_2 """ if vparity not in (1, -1): raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - rel_angle = roll_ref - (vparity * v3i_yang) + rel_angle = roll_ref - (vparity * v3i_yangle) pc1_1 = vparity * np.cos(rel_angle) pc1_2 = np.sin(rel_angle) @@ -149,8 +146,22 @@ def calc_rotation_matrix( def _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes -): + spatial_footprint: np.ndarray, +) -> np.ndarray: + """ + Calculates the fiducial coordinates from a given spatial footprint. + + Parameters + ---------- + spatial_footprint : `~numpy.ndarray` + A 2xN array containing the world coordinates of the WCS footprint's + bounding box, where N is the number of bounding box positions. + + Returns + ------- + lon_fiducial, lat_fiducial : `numpy.ndarray`, `numpy.ndarray` + The world coordinates of the fiducial point in the output coordinate frame. + """ lon, lat = spatial_footprint lon, lat = np.deg2rad(lon), np.deg2rad(lat) x = np.cos(lat) * np.cos(lon) @@ -164,14 +175,34 @@ def _calculate_fiducial_from_spatial_footprint( lat_fiducial = np.rad2deg( np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) ) - fiducial[spatial_axes] = lon_fiducial, lat_fiducial + return lon_fiducial, lat_fiducial -def compute_fiducial(wcslist, bounding_box=None): +def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: """ - For a celestial footprint this is the center. - For a spectral footprint, it is the beginning of the range. + Calculates the world coordinates of the fiducial point of a list of WCS objects. + For a celestial footprint this is the center. For a spectral footprint, it is the + beginning of its range. + + Parameters + ---------- + wcslist : list + A list containing all the WCS objects for which the fiducial is to be + calculated. + + bounding_box : `~astropy.modeling.bounding_box` or list, optional + The bounding box to be used when calculating the fiducial. + If a list is provided, it should be in the following format: + [[x0_lower, x0_upper], [x1_lower, x1_upper]]. + Returns + ------- + fiducial : `numpy.ndarray` + A two-elements array containing the world coordinates of the fiducial point + in the combined output coordinate frame. + + Notes + ----- This function assumes all WCSs have the same output coordinate frame. """ @@ -186,8 +217,8 @@ def compute_fiducial(wcslist, bounding_box=None): fiducial = np.empty(len(axes_types)) if spatial_footprint.any(): - _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes + fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( + spatial_footprint ) if spectral_footprint.any(): fiducial[spectral_axes] = spectral_footprint.min() @@ -196,12 +227,17 @@ def compute_fiducial(wcslist, bounding_box=None): def wcsinfo_from_model(input_model: SupportsDataWithWcs): """ - Create a dict {wcs_keyword: array_of_values} pairs from a data model. + Creates a dict {wcs_keyword: array_of_values} pairs from a data model. Parameters ---------- input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` - The input data model + The input data model. + + Returns + ------- + wcsinfo : dict + A dict containing the WCS FITS keywords and corresponding values. """ defaults = { @@ -221,7 +257,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): val.append(v) wcsinfo[key] = np.array(val) - pc = np.zeros((wcsaxes, wcsaxes)) + pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) for i in range(1, wcsaxes + 1): for j in range(1, wcsaxes + 1): pc[i - 1, j - 1] = getattr( @@ -234,8 +270,45 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): def _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial + refmodel: SupportsDataWithWcs, + ref_fiducial: np.array, + pscale_ratio: int = None, + pscale: float = None, + rotation: float = None, ): + """ + Creates a transform from pixel to world coordinates based on a + reference datamodel's WCS. + + Parameters + ---------- + refmodel : a valid datamodel + The datamodel that should be used as reference for calculating the + transform parameters. + pscale_ratio : int, None, optional + Ratio of input to output pixel scale. This parameter is only used when + pscale=`None` and, in that case, it is passed on to `compute_scale`. + pscale : float, None, optional + The plate scale. If `None`, the plate scale is calculated from the reference + datamodel. + rotation : float, None, optional + Position angle of output image's Y-axis relative to North. + A value of 0.0 would orient the final output image to be North up. + The default of `None` specifies that the images will not be rotated, + but will instead be resampled in the default orientation for the camera + with the x and y axes of the resampled image corresponding + approximately to the detector axes. Ignored when ``transform`` is + provided. If `None`, the rotation angle is extracted from the + reference model's `meta.wcsinfo.roll_ref`. + ref_fiducial : np.array + A two-elements array containing the world coordinates of the fiducial point. + + Returns + ------- + transform : `~astropy.modeling.core.CompoundModel` + An `~astropy.modeling` compound model containing the transform from pixel to + world coordinates. + """ sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -298,9 +371,9 @@ def wcs_from_footprints( Parameters ---------- - dmodels : list of `~jwst.datamodels.JwstDataModel` + dmodels : list of valid datamodels A list of data models. - refmodel : `~jwst.datamodels.JwstDataModel`, optional + refmodel : a valid datamodel, optional This model's WCS is used as a reference. WCS. The output coordinate frame, the projection and a scaling and rotation transform is created from it. If not supplied @@ -338,6 +411,11 @@ def wcs_from_footprints( Right ascension and declination of the reference pixel. Automatically computed if not provided. + Returns + ------- + wnew : `~gwcs.WCS` + The WCS object associated with the combined input footprints. + """ bb = bounding_box wcslist = [im.meta.wcs for im in dmodels] @@ -368,7 +446,11 @@ def wcs_from_footprints( if transform is None: transform = _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial + refmodel=refmodel, + pscale_ratio=pscale_ratio, + pscale=pscale, + rotation=rotation, + ref_fiducial=ref_fiducial, ) out_frame = refmodel.meta.wcs.output_frame diff --git a/tests/test_alignment.py b/tests/test_alignment.py index ce4c6863..7aaba7aa 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -21,22 +21,16 @@ def _create_wcs_object_without_distortion( pscale, shape, ): - fiducial_detector = tuple(shape.value) - # subtract 1 to account for pixel indexing starting at 0 - shift = models.Shift(-(fiducial_detector[0] - 1)) & models.Shift( - -(fiducial_detector[1] - 1) - ) + shift = models.Shift(-(shape[0] - 1)) & models.Shift(-(shape[1] - 1)) - scale = models.Scale(pscale[0].to("deg")) & models.Scale( - pscale[1].to("deg") - ) + scale = models.Scale(pscale[0]) & models.Scale(pscale[1]) tan = models.Pix2Sky_TAN() celestial_rotation = models.RotateNative2Celestial( fiducial_world[0], fiducial_world[1], - 180 * u.deg, + 180, ) det2sky = shift | scale | tan | celestial_rotation @@ -54,8 +48,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (-0.5, fiducial_detector[0] - 0.5), - (-0.5, fiducial_detector[0] - 0.5), + (-0.5, shape[0] - 0.5), + (-0.5, shape[0] - 0.5), ) return wcs_obj @@ -65,7 +59,7 @@ def _create_wcs_and_datamodel(fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value + ra_ref, dec_ref = fiducial_world[0], fiducial_world[1] return DataModel( ra_ref=ra_ref, dec_ref=dec_ref, @@ -121,9 +115,9 @@ def test_compute_fiducial(): WCS's footprint. """ - shape = (3, 3) * u.pix - fiducial_world = (0, 0) * u.deg - pscale = (0.05, 0.05) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (0, 0) # in deg + pscale = (0.000014, 0.000014) # in deg/pixel wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale @@ -134,35 +128,40 @@ def test_compute_fiducial(): assert all(np.isclose(wcs(1, 1), computed_fiducial)) -@pytest.mark.parametrize("pscales", [(0.05, 0.05), (0.1, 0.05)]) +@pytest.mark.parametrize( + "pscales", [(0.000014, 0.000014), (0.000028, 0.000014)] +) def test_compute_scale(pscales): """Test that util.compute_scale can properly determine the pixel scale of a WCS object. """ - shape = (3, 3) * u.pix - fiducial_world = (0, 0) * u.deg - pscale = (pscales[0], pscales[1]) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (0, 0) # in deg + pscale = (pscales[0], pscales[1]) # in deg/pixel wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - expected_scale = np.sqrt(pscale[0].to("deg") * pscale[1].to("deg")).value + expected_scale = np.sqrt(pscale[0] * pscale[1]) - computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world.value) + computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world) assert np.isclose(expected_scale, computed_scale) def test_wcs_from_footprints(): - shape = (3, 3) * u.pix - fiducial_world = (10, 0) * u.deg - pscale = (0.1, 0.1) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (10, 0) # in deg + pscale = (0.000028, 0.000028) # in deg/pixel dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs - # new fiducial will be shifted by one pixel in both directions - fiducial_world -= pscale + # shift fiducial by one pixel in both directions and create a new WCS + fiducial_world = ( + fiducial_world[0] - 0.000028, + fiducial_world[1] - 0.000028, + ) dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs From 0cd3621d2a62d1561cb90b663b2eccc8c59c0092 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 11:26:08 -0400 Subject: [PATCH 024/117] Fix misplaced comment. --- src/stcal/alignment/util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 134b0e65..1108692c 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -310,10 +310,6 @@ def _generate_tranform_from_datamodel( world coordinates. """ sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - - # Need to put the rotation matrix (List[float, float, float, float]) - # returned from calc_rotation_matrix into the correct shape for - # constructing the transformation v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) vparity = refmodel.meta.wcsinfo.vparity if rotation is None: @@ -321,6 +317,8 @@ def _generate_tranform_from_datamodel( else: roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + # reshape the rotation matrix returned from calc_rotation_matrix + # into the correct shape for constructing the transformation pc = np.reshape( calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) ) From a8839a9392e151f1d735b5a1226fbfd8e26b10de Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 11:39:23 -0400 Subject: [PATCH 025/117] Code refactoring and additional unit test. --- src/stcal/alignment/util.py | 415 ++++++++++++++++++++++++++---------- tests/test_alignment.py | 41 ++++ 2 files changed, 349 insertions(+), 107 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 1108692c..a2df71a4 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -190,10 +190,12 @@ def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: A list containing all the WCS objects for which the fiducial is to be calculated. - bounding_box : `~astropy.modeling.bounding_box` or list, optional - The bounding box to be used when calculating the fiducial. - If a list is provided, it should be in the following format: - [[x0_lower, x0_upper], [x1_lower, x1_upper]]. + bounding_box : tuple, or list, optional + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. Returns ------- @@ -269,12 +271,13 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): return wcsinfo -def _generate_tranform_from_datamodel( +def _generate_tranform( refmodel: SupportsDataWithWcs, ref_fiducial: np.array, pscale_ratio: int = None, pscale: float = None, rotation: float = None, + transform=None, ): """ Creates a transform from pixel to world coordinates based on a @@ -285,12 +288,15 @@ def _generate_tranform_from_datamodel( refmodel : a valid datamodel The datamodel that should be used as reference for calculating the transform parameters. + pscale_ratio : int, None, optional Ratio of input to output pixel scale. This parameter is only used when pscale=`None` and, in that case, it is passed on to `compute_scale`. + pscale : float, None, optional The plate scale. If `None`, the plate scale is calculated from the reference datamodel. + rotation : float, None, optional Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. @@ -300,44 +306,282 @@ def _generate_tranform_from_datamodel( approximately to the detector axes. Ignored when ``transform`` is provided. If `None`, the rotation angle is extracted from the reference model's `meta.wcsinfo.roll_ref`. + ref_fiducial : np.array A two-elements array containing the world coordinates of the fiducial point. + transform : `~astropy.modeling.Model`, optional + A transform between frames. + + Returns + ------- + transform : `~astropy.modeling.Model` + An `~astropy` model containing the transform between frames. + """ + if transform is None: + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) + vparity = refmodel.meta.wcsinfo.vparity + if rotation is None: + roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + else: + roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + + # reshape the rotation matrix returned from calc_rotation_matrix + # into the correct shape for constructing the transformation + pc = np.reshape( + calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + ) + + rotation = astmodels.AffineTransformation2D( + pc, name="pc_rotation_matrix" + ) + transform = [rotation] + if sky_axes: + if not pscale: + pscale = compute_scale( + refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio + ) + transform.append( + astmodels.Scale(pscale, name="cdelt1") + & astmodels.Scale(pscale, name="cdelt2") + ) + + if transform: + transform = functools.reduce(lambda x, y: x | y, transform) + + return transform + + +def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): + """ + Calculates axis mininum values and bounding box. + + Parameters + ---------- + ref_model : a valid datamodel + The reference datamodel for which to determine the minimum axis values and + bounding box. + + wcs_list : list + The list of WCS objects. + + ref_wcs : `~gwcs.wcs.WCS` + The reference WCS object. + + Returns + ------- + tuple + A tuple containing two elements: + 1 - a `numpy.array` with the minimum value in each axis; + 2 - a tuple containing the bounding box region in the format + ((x0_lower, x0_upper), (x1_lower, x1_upper)). + """ + footprints = [w.footprint().T for w in wcs_list] + domain_bounds = np.hstack( + [ref_wcs.backward_transform(*f) for f in footprints] + ) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in ref_model.meta.wcs.output_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + # populate output_bounding_box + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + return (axis_min_values, output_bounding_box) + + +def _calculate_fiducial(wcs_list, bounding_box, crval=None): + """ + Calculates the coordinates of the fiducial point and, if necessary, updates it with + the values in CRVAL (the update is applied to spatial axes only). + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + bounding_box : tuple, or list, optional + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + crval : list, optional + A reference world coordinate associated with the reference pixel. If not `None`, + then the fiducial coordinates of the spatial axes will be updated with the + values from `crval`. + Returns ------- - transform : `~astropy.modeling.core.CompoundModel` - An `~astropy.modeling` compound model containing the transform from pixel to - world coordinates. + fiducial : `~numpy.ndarray` + A two-elements array containing the world coordinate of the fiducial point. """ - sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) - vparity = refmodel.meta.wcsinfo.vparity - if rotation is None: - roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) + if crval is not None: + i = 0 + for k, axt in enumerate(wcs_list[0].output_frame.axes_type): + if axt == "SPATIAL": + # overwrite only spatial axes with user-provided CRVAL + fiducial[k] = crval[i] + i += 1 + return fiducial + + +def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): + """ + Calculates the offsets to the transform. + + Parameters + ---------- + fiducial : `~numpy.ndarray` + A two-elements containing the world coordinates of the fiducial point. + + wcs : `~gwcs.wcs.WCS` + A WCS object. It will be used to determine the + + axis_min_values : `~numpy.ndarray` + A two-elements array containing the minimum pixel value for each axis. + + crpix : list or tuple + Pixel coordinates of the reference pixel. + + Returns + ------- + `~astropy.modeling.Model` + A model with the offsets to be added to the WCS's transform. + + Notes + ----- + If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. + The reason being that, in this case, the offsets will be calculated using the + WCS object to find the pixel coordinates of the fiducial point and then correct it + by the minimum pixel value for each axis. + """ + if ( + crpix is None + and fiducial is not None + and wcs is not None + and axis_min_values is not None + ): + offset1, offset2 = wcs.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] else: - roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + offset1, offset2 = crpix - # reshape the rotation matrix returned from calc_rotation_matrix - # into the correct shape for constructing the transformation - pc = np.reshape( - calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + return astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" ) - rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") - transform = [rotation] - if sky_axes: - if not pscale: - pscale = compute_scale( - refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio - ) - transform.append( - astmodels.Scale(pscale, name="cdelt1") - & astmodels.Scale(pscale, name="cdelt2") + +def _calculate_new_wcs( + ref_model, shape, wcs_list, fiducial, crpix=None, transform=None +): + """ + Calculates a new WCS object based on the combined WCS objects provided. + + Parameters + ---------- + ref_model : a valid datamodel + The reference model to be used when extracting metadata. + + shape : list + The shape of the new WCS's pixel grid. If `None`, then the output bounding box + will be used to determine it. + + wcs_list : list + A list containing WCS objects. + + fiducial : `~numpy.ndarray` + A two-elements array containing the location on the sky in some standard + coordinate system. + + crpix : tuple, optional + The coordinates of the reference pixel. + + transform : `~astropy.modeling.Model`, optional + An optional tranform to be prepended to the transform constructed by the + fiducial point. The number of outputs of this transform must equal the number + of axes in the coordinate frame. + + Returns + ------- + `~gwcs.wcs.WCS` + The new WCS object that corresponds to the combined WCS objects in `wcs_list`. + """ + wcs_new = wcs_from_fiducial( + fiducial, + coordinate_frame=ref_model.meta.wcs.output_frame, + projection=astmodels.Pix2Sky_TAN(), + transform=transform, + input_frame=ref_model.meta.wcs.input_frame, + ) + axis_min_values, output_bounding_box = _get_axis_min_and_bounding_box( + ref_model, wcs_list, wcs_new + ) + offsets = _calculate_offsets( + fiducial=fiducial, + wcs=wcs_new, + axis_min_values=axis_min_values, + crpix=crpix, + ) + + wcs_new.insert_transform("detector", offsets, after=True) + wcs_new.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wcs_new.pixel_shape = shape[::-1] + wcs_new.array_shape = shape + return wcs_new + + +def _validate_wcs_list(wcs_list): + """ + Validates wcs_list. + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + Returns + ------- + bool or Exception + If wcs_list is valid, returns True. Otherwise, it will raise an error. + + Raises + ------ + ValueError + Raised whenever wcs_list is not an iterable. + TypeError + Raised whenever wcs_list is empty or any of its content is not an + instance of WCS. + """ + if not isiterable(wcs_list): + raise ValueError( + "Expected 'wcs_list' to be an iterable of WCS objects." ) + elif len(wcs_list): + if not all(isinstance(w, WCS) for w in wcs_list): + raise TypeError( + "All items in 'wcs_list' are to be instances of gwcs.WCS." + ) + else: + raise TypeError("'wcs_list' should not be empty.") - if transform: - transform = functools.reduce(lambda x, y: x | y, transform) - return transform + return True def wcs_from_footprints( @@ -364,30 +608,36 @@ def wcs_from_footprints( is taken from ``refmodel``. If ``transform`` is not supplied, a compound transform is created using CDELTs and PC. - If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed - from bounding_box of all input WCSs. + If ``bounding_box`` is not supplied, the `bounding_box` of the new WCS is computed + from `bounding_box` of all input WCSs. Parameters ---------- dmodels : list of valid datamodels A list of data models. + refmodel : a valid datamodel, optional This model's WCS is used as a reference. WCS. The output coordinate frame, the projection and a scaling and rotation transform is created from it. If not supplied the first model in the list is used as ``refmodel``. + transform : `~astropy.modeling.core.Model`, optional A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` If not supplied Scaling | Rotation is computed from ``refmodel``. + bounding_box : tuple, optional Bounding_box of the new WCS. If not supplied it is computed from the bounding_box of all inputs. + pscale_ratio : float, None, optional Ratio of input to output pixel scale. Ignored when either ``transform`` or ``pscale`` are provided. + pscale : float, None, optional Absolute pixel scale in degrees. When provided, overrides ``pscale_ratio``. Ignored when ``transform`` is provided. + rotation : float, None, optional Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. @@ -396,104 +646,55 @@ def wcs_from_footprints( with the x and y axes of the resampled image corresponding approximately to the detector axes. Ignored when ``transform`` is provided. + shape : tuple of int, None, optional Shape of the image (data array) using ``numpy.ndarray`` convention (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. + crpix : tuple of float, None, optional Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. + crval : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. Returns ------- - wnew : `~gwcs.WCS` - The WCS object associated with the combined input footprints. + wcs_new : `~gwcs.wcs.WCS` + The WCS object corresponding to the combined input footprints. """ - bb = bounding_box - wcslist = [im.meta.wcs for im in dmodels] - - if not isiterable(wcslist): - raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") - if not all(isinstance(w, WCS) for w in wcslist): - raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") + wcs_list = [im.meta.wcs for im in dmodels] - if refmodel is None: - refmodel = dmodels[0] - - fiducial = compute_fiducial(wcslist, bb) - if crval is not None: - # overwrite spatial axes with user-provided CRVAL: - i = 0 - for k, axt in enumerate(wcslist[0].output_frame.axes_type): - if axt == "SPATIAL": - fiducial[k] = crval[i] - i += 1 + _validate_wcs_list(wcs_list) - ref_fiducial = np.array( - [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + fiducial = _calculate_fiducial( + wcs_list=wcs_list, bounding_box=bounding_box, crval=crval ) - prj = astmodels.Pix2Sky_TAN() + refmodel = dmodels[0] if refmodel is None else refmodel - if transform is None: - transform = _generate_tranform_from_datamodel( - refmodel=refmodel, - pscale_ratio=pscale_ratio, - pscale=pscale, - rotation=rotation, - ref_fiducial=ref_fiducial, - ) - - out_frame = refmodel.meta.wcs.output_frame - input_frame = refmodel.meta.wcs.input_frame - wnew = wcs_from_fiducial( - fiducial, - coordinate_frame=out_frame, - projection=prj, + transform = _generate_tranform( + refmodel=refmodel, + pscale_ratio=pscale_ratio, + pscale=pscale, + rotation=rotation, + ref_fiducial=np.array( + [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + ), transform=transform, - input_frame=input_frame, ) - footprints = [w.footprint().T for w in wcslist] - domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) - axis_min_values = np.min(domain_bounds, axis=1) - domain_bounds = (domain_bounds.T - axis_min_values).T - - output_bounding_box = [] - for axis in out_frame.axes_order: - axis_min, axis_max = ( - domain_bounds[axis].min(), - domain_bounds[axis].max(), - ) - output_bounding_box.append((axis_min, axis_max)) - - output_bounding_box = tuple(output_bounding_box) - if crpix is None: - offset1, offset2 = wnew.backward_transform(*fiducial) - offset1 -= axis_min_values[0] - offset2 -= axis_min_values[1] - else: - offset1, offset2 = crpix - offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" + return _calculate_new_wcs( + ref_model=refmodel, + shape=shape, + crpix=crpix, + wcs_list=wcs_list, + fiducial=fiducial, + transform=transform, ) - - wnew.insert_transform("detector", offsets, after=True) - wnew.bounding_box = output_bounding_box - - if shape is None: - shape = [ - int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] - ] - - wnew.pixel_shape = shape[::-1] - wnew.array_shape = shape - - return wnew diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 7aaba7aa..70138581 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -13,6 +13,7 @@ compute_fiducial, compute_scale, wcs_from_footprints, + _validate_wcs_list, ) @@ -177,3 +178,43 @@ def test_wcs_from_footprints(): # expected position onto wcs_1 and wcs_2 assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + + +def test_validate_wcs_list(): + shape = (3, 3) # in pixels + fiducial_world = (10, 0) # in deg + pscale = (0.000028, 0.000028) # in deg/pixel + + dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) + wcs_1 = dm_1.meta.wcs + + # shift fiducial by one pixel in both directions and create a new WCS + fiducial_world = ( + fiducial_world[0] - 0.000028, + fiducial_world[1] - 0.000028, + ) + dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) + wcs_2 = dm_2.meta.wcs + + wcs_list = [wcs_1, wcs_2] + + assert _validate_wcs_list(wcs_list) == True + + +@pytest.mark.parametrize( + "wcs_list, expected_error", + [ + ([], TypeError), + ([1, 2, 3], TypeError), + (["1", "2", "3"], TypeError), + (["1", None, []], TypeError), + ("1", TypeError), + (1, ValueError), + (None, ValueError), + ], +) +def test_validate_wcs_list_invalid(wcs_list, expected_error): + with pytest.raises(Exception) as exec_info: + result = _validate_wcs_list(wcs_list) + + assert type(exec_info.value) == expected_error From 47f785b096ebe5e453e442c2980e74241ed95a1d Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:27:18 -0400 Subject: [PATCH 026/117] Silencing style check warning F403. --- src/stcal/alignment/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/__init__.py b/src/stcal/alignment/__init__.py index 46d3a156..e870d4bd 100644 --- a/src/stcal/alignment/__init__.py +++ b/src/stcal/alignment/__init__.py @@ -1 +1 @@ -from .util import * +from .util import * # noqa: F403 From 424fd385bf7281c3be180ba94a3fa24118f18942 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:28:01 -0400 Subject: [PATCH 027/117] Small style refactoring. --- tests/test_alignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 70138581..5f7ab792 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -198,7 +198,7 @@ def test_validate_wcs_list(): wcs_list = [wcs_1, wcs_2] - assert _validate_wcs_list(wcs_list) == True + assert _validate_wcs_list(wcs_list) @pytest.mark.parametrize( @@ -215,6 +215,6 @@ def test_validate_wcs_list(): ) def test_validate_wcs_list_invalid(wcs_list, expected_error): with pytest.raises(Exception) as exec_info: - result = _validate_wcs_list(wcs_list) + _validate_wcs_list(wcs_list) assert type(exec_info.value) == expected_error From d67e0b98f0878844eec48b77d939b8231c9b1a68 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:31:59 -0400 Subject: [PATCH 028/117] Set minimum asdf version. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9522b45c..1acb2105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', - 'asdf', + 'asdf >=2.15.0', 'gwcs', ] dynamic = ['version'] From 1229f0261196124ef5fd145562ebfdf327e95037 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:43:14 -0400 Subject: [PATCH 029/117] Update matplotlib's inventory URL. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d0fe5754..58e3f7d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ def setup(app): "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/devdocs", None), "scipy": ("http://scipy.github.io/devdocs", None), - "matplotlib": ("http://matplotlib.org/", None), + "matplotlib": ("https://matplotlib.org/stable", None), } extensions = [ From 500479ae88bdd1078c82a71e68e99de3a4a1b662 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 16:04:17 -0400 Subject: [PATCH 030/117] Set lower version for gwcs. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1acb2105..70cd5673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs', + 'gwcs >=0.18.3', ] dynamic = ['version'] From eeccb1b83601b277655a453fbb9de7690f1307aa Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 16:18:36 -0400 Subject: [PATCH 031/117] Bump astropy version to >= 5.1. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 70cd5673..36eb101c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,12 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.0.4', + 'astropy >=5.1', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs >=0.18.3', + 'gwcs >= 0.18.0', ] dynamic = ['version'] From 7e6dbf3721b8dab776c581730ccb78bd3b2f7769 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 14 Jul 2023 12:27:03 -0400 Subject: [PATCH 032/117] Fix docs style issues. --- docs/Makefile | 2 + docs/conf.py | 19 +- pyproject.toml | 7 +- src/stcal/alignment/util.py | 504 ++++++++++++++++++------------------ 4 files changed, 276 insertions(+), 256 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index bcca5213..1235f237 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,6 +6,7 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build BUILDDIR = _build +APIDIR = api # Internal variables ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . @@ -25,6 +26,7 @@ help: clean: -rm -rf $(BUILDDIR)/* + -rm -rf $(APIDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/conf.py b/docs/conf.py index 58e3f7d3..5ebcee52 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,12 +45,27 @@ def setup(app): "numpy": ("https://numpy.org/devdocs", None), "scipy": ("http://scipy.github.io/devdocs", None), "matplotlib": ("https://matplotlib.org/stable", None), + "gwcs": ("https://gwcs.readthedocs.io/en/latest/", None), + "astropy": ("https://docs.astropy.org/en/stable/", None), + "stdatamodels": ("https://stdatamodels.readthedocs.io/en/latest/", None), } extensions = [ - "sphinx_automodapi.automodapi", + "pytest_doctestplus.sphinx.doctestplus", + "sphinx.ext.autodoc", "sphinx.ext.intersphinx", - "numpydoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx_automodapi.automodapi", + "sphinx_automodapi.automodsumm", + "sphinx_automodapi.autodoc_enhancements", + "sphinx_automodapi.smart_resolver", + "sphinx_asdf", + "sphinx.ext.mathjax", ] autosummary_generate = True diff --git a/pyproject.toml b/pyproject.toml index 36eb101c..b2e1b99c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,12 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.1', + 'astropy >=5.0.4', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs >= 0.18.0', + 'gwcs >= 0.18.1', ] dynamic = ['version'] @@ -26,10 +26,11 @@ docs = [ 'numpydoc', 'packaging >=17', 'sphinx', + 'sphinx-asdf', 'sphinx-astropy', 'sphinx-rtd-theme', 'stsci-rtd-theme', - 'tomli; python_version <"3.11"', + 'tomli; python_version <="3.11"', ] test = [ 'psutil', diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index a2df71a4..39697617 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -23,10 +23,10 @@ __all__ = [ - "wcs_from_footprints", "compute_scale", "compute_fiducial", "calc_rotation_matrix", + "wcs_from_footprints", ] @@ -37,114 +37,6 @@ def to_flat_dict(): ... -def compute_scale( - wcs: WCS, - fiducial: Union[tuple, np.ndarray], - disp_axis: int = None, - pscale_ratio: float = None, -) -> float: - """Compute scaling transform. - - Parameters - ---------- - wcs : `~gwcs.wcs.WCS` - Reference WCS object from which to compute a scaling factor. - - fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating - reference points. - - disp_axis : int - Dispersion axis integer. Assumes the same convention as - `wcsinfo.dispersion_direction` - - pscale_ratio : int - Ratio of input to output pixel scale - - Returns - ------- - scale : float - Scaling factor for x and y or cross-dispersion direction. - - """ - spectral = "SPECTRAL" in wcs.output_frame.axes_type - - if spectral and disp_axis is None: - raise ValueError("If input WCS is spectral, a disp_axis must be given") - - crpix = np.array(wcs.invert(*fiducial)) - - delta = np.zeros_like(crpix) - spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] - delta[spatial_idx[0]] = 1 - - crpix_with_offsets = np.vstack( - (crpix, crpix + delta, crpix + np.roll(delta, 1)) - ).T - crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) - - coords = SkyCoord( - ra=crval_with_offsets[spatial_idx[0]], - dec=crval_with_offsets[spatial_idx[1]], - unit="deg", - ) - xscale = np.abs(coords[0].separation(coords[1]).value) - yscale = np.abs(coords[0].separation(coords[2]).value) - - if pscale_ratio is not None: - xscale *= pscale_ratio - yscale *= pscale_ratio - - if spectral: - # Assuming scale doesn't change with wavelength - # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction - return yscale if disp_axis == 1 else xscale - - return np.sqrt(xscale * yscale) - - -def calc_rotation_matrix( - roll_ref: float, v3i_yangle: float, vparity: int = 1 -) -> List[float]: - """Calculate the rotation matrix. - - Parameters - ---------- - roll_ref : float - Telescope roll angle of V3 North over East at the ref. point in radians - - v3i_yangle : float - The angle between ideal Y-axis and V3 in radians. - - vparity : int - The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. - Value should be "1" or "-1". - - Returns - ------- - matrix: [pc1_1, pc1_2, pc2_1, pc2_2] - The rotation matrix - - Notes - ----- - The rotation is - pc1_1 | pc2_1 - - pc1_2 | pc2_2 - """ - if vparity not in (1, -1): - raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - - rel_angle = roll_ref - (vparity * v3i_yangle) - - pc1_1 = vparity * np.cos(rel_angle) - pc1_2 = np.sin(rel_angle) - pc2_1 = vparity * -np.sin(rel_angle) - pc2_2 = np.cos(rel_angle) - - return [pc1_1, pc1_2, pc2_1, pc2_2] - - def _calculate_fiducial_from_spatial_footprint( spatial_footprint: np.ndarray, ) -> np.ndarray: @@ -153,13 +45,13 @@ def _calculate_fiducial_from_spatial_footprint( Parameters ---------- - spatial_footprint : `~numpy.ndarray` + spatial_footprint : numpy.ndarray A 2xN array containing the world coordinates of the WCS footprint's bounding box, where N is the number of bounding box positions. Returns ------- - lon_fiducial, lat_fiducial : `numpy.ndarray`, `numpy.ndarray` + lon_fiducial, lat_fiducial : numpy.ndarray, numpy.ndarray The world coordinates of the fiducial point in the output coordinate frame. """ lon, lat = spatial_footprint @@ -178,99 +70,6 @@ def _calculate_fiducial_from_spatial_footprint( return lon_fiducial, lat_fiducial -def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: - """ - Calculates the world coordinates of the fiducial point of a list of WCS objects. - For a celestial footprint this is the center. For a spectral footprint, it is the - beginning of its range. - - Parameters - ---------- - wcslist : list - A list containing all the WCS objects for which the fiducial is to be - calculated. - - bounding_box : tuple, or list, optional - The bounding box over which the WCS is valid. It can be a either tuple of tuples - or a list of lists of size 2 where each element represents a range of - (low, high) values. The bounding_box is in the order of the axes, axes_order. - For two inputs and axes_order(0, 1) the bounding box can be either - ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. - - Returns - ------- - fiducial : `numpy.ndarray` - A two-elements array containing the world coordinates of the fiducial point - in the combined output coordinate frame. - - Notes - ----- - This function assumes all WCSs have the same output coordinate frame. - """ - - axes_types = wcslist[0].output_frame.axes_type - spatial_axes = np.array(axes_types) == "SPATIAL" - spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack( - [w.footprint(bounding_box=bounding_box).T for w in wcslist] - ) - spatial_footprint = footprints[spatial_axes] - spectral_footprint = footprints[spectral_axes] - - fiducial = np.empty(len(axes_types)) - if spatial_footprint.any(): - fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( - spatial_footprint - ) - if spectral_footprint.any(): - fiducial[spectral_axes] = spectral_footprint.min() - return fiducial - - -def wcsinfo_from_model(input_model: SupportsDataWithWcs): - """ - Creates a dict {wcs_keyword: array_of_values} pairs from a data model. - - Parameters - ---------- - input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` - The input data model. - - Returns - ------- - wcsinfo : dict - A dict containing the WCS FITS keywords and corresponding values. - - """ - defaults = { - "CRPIX": 0, - "CRVAL": 0, - "CDELT": 1.0, - "CTYPE": "", - "CUNIT": u.Unit(""), - } - wcsaxes = input_model.meta.wcsinfo.wcsaxes - wcsinfo = {"WCSAXES": wcsaxes} - for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: - val = [] - for ax in range(1, wcsaxes + 1): - k = (key + "{0}".format(ax)).lower() - v = getattr(input_model.meta.wcsinfo, k, defaults[key]) - val.append(v) - wcsinfo[key] = np.array(val) - - pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) - for i in range(1, wcsaxes + 1): - for j in range(1, wcsaxes + 1): - pc[i - 1, j - 1] = getattr( - input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 - ) - wcsinfo["PC"] = pc - wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame - wcsinfo["has_cd"] = False - return wcsinfo - - def _generate_tranform( refmodel: SupportsDataWithWcs, ref_fiducial: np.array, @@ -285,19 +84,19 @@ def _generate_tranform( Parameters ---------- - refmodel : a valid datamodel + refmodel : The datamodel that should be used as reference for calculating the transform parameters. - pscale_ratio : int, None, optional + pscale_ratio : int, None Ratio of input to output pixel scale. This parameter is only used when - pscale=`None` and, in that case, it is passed on to `compute_scale`. + ``pscale=None`` and, in that case, it is passed on to ``compute_scale``. - pscale : float, None, optional + pscale : float, None The plate scale. If `None`, the plate scale is calculated from the reference datamodel. - rotation : float, None, optional + rotation : float, None Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. The default of `None` specifies that the images will not be rotated, @@ -305,18 +104,18 @@ def _generate_tranform( with the x and y axes of the resampled image corresponding approximately to the detector axes. Ignored when ``transform`` is provided. If `None`, the rotation angle is extracted from the - reference model's `meta.wcsinfo.roll_ref`. + reference model's ``meta.wcsinfo.roll_ref``. - ref_fiducial : np.array + ref_fiducial : numpy.array A two-elements array containing the world coordinates of the fiducial point. - transform : `~astropy.modeling.Model`, optional + transform : ~astropy.modeling.Model A transform between frames. Returns ------- - transform : `~astropy.modeling.Model` - An `~astropy` model containing the transform between frames. + transform : ~astropy.modeling.Model + An :py:mod:`~astropy` model containing the transform between frames. """ if transform is None: sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() @@ -359,21 +158,21 @@ def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference datamodel for which to determine the minimum axis values and bounding box. wcs_list : list The list of WCS objects. - ref_wcs : `~gwcs.wcs.WCS` + ref_wcs : ~gwcs.wcs.WCS The reference WCS object. Returns ------- tuple A tuple containing two elements: - 1 - a `numpy.array` with the minimum value in each axis; + 1 - a :py:class:`numpy.ndarray` with the minimum value in each axis; 2 - a tuple containing the bounding box region in the format ((x0_lower, x0_upper), (x1_lower, x1_upper)). """ @@ -417,11 +216,11 @@ def _calculate_fiducial(wcs_list, bounding_box, crval=None): crval : list, optional A reference world coordinate associated with the reference pixel. If not `None`, then the fiducial coordinates of the spatial axes will be updated with the - values from `crval`. + values from ``crval``. Returns ------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the world coordinate of the fiducial point. """ fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) @@ -441,13 +240,13 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Parameters ---------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements containing the world coordinates of the fiducial point. - wcs : `~gwcs.wcs.WCS` + wcs : ~gwcs.wcs.WCS A WCS object. It will be used to determine the - axis_min_values : `~numpy.ndarray` + axis_min_values : numpy.ndarray A two-elements array containing the minimum pixel value for each axis. crpix : list or tuple @@ -455,15 +254,15 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Returns ------- - `~astropy.modeling.Model` + ~astropy.modeling.Model A model with the offsets to be added to the WCS's transform. Notes ----- - If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. - The reason being that, in this case, the offsets will be calculated using the - WCS object to find the pixel coordinates of the fiducial point and then correct it - by the minimum pixel value for each axis. + If ``crpix=None``, then ``fiducial``, ``wcs``, and ``axis_min_values`` must be + provided, in which case, the offsets will be calculated using the WCS object to + find the pixel coordinates of the fiducial point and then correct it by the minimum + pixel value for each axis. """ if ( crpix is None @@ -490,7 +289,7 @@ def _calculate_new_wcs( Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference model to be used when extracting metadata. shape : list @@ -500,21 +299,21 @@ def _calculate_new_wcs( wcs_list : list A list containing WCS objects. - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the location on the sky in some standard coordinate system. crpix : tuple, optional The coordinates of the reference pixel. - transform : `~astropy.modeling.Model`, optional + transform : ~astropy.modeling.Model An optional tranform to be prepended to the transform constructed by the fiducial point. The number of outputs of this transform must equal the number of axes in the coordinate frame. Returns ------- - `~gwcs.wcs.WCS` + wcs_new : ~gwcs.wcs.WCS The new WCS object that corresponds to the combined WCS objects in `wcs_list`. """ wcs_new = wcs_from_fiducial( @@ -576,7 +375,7 @@ def _validate_wcs_list(wcs_list): elif len(wcs_list): if not all(isinstance(w, WCS) for w in wcs_list): raise TypeError( - "All items in 'wcs_list' are to be instances of gwcs.WCS." + "All items in 'wcs_list' are to be instances of gwcs.wcs.WCS." ) else: raise TypeError("'wcs_list' should not be empty.") @@ -584,6 +383,210 @@ def _validate_wcs_list(wcs_list): return True +def wcsinfo_from_model(input_model: SupportsDataWithWcs): + """ + Creates a dict {wcs_keyword: array_of_values} pairs from a datamodel. + + Parameters + ---------- + input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + The input datamodel. + + Returns + ------- + wcsinfo : dict + A dict containing the WCS FITS keywords and corresponding values. + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : ~gwcs.wcs.WCS + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating + reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as + ``wcsinfo.dispersion_direction`` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: + """ + Calculates the world coordinates of the fiducial point of a list of WCS objects. + For a celestial footprint this is the center. For a spectral footprint, it is the + beginning of its range. + + Parameters + ---------- + wcslist : list + A list containing all the WCS objects for which the fiducial is to be + calculated. + + bounding_box : tuple, list, None + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + Returns + ------- + fiducial : numpy.ndarray + A two-elements array containing the world coordinates of the fiducial point + in the combined output coordinate frame. + + Notes + ----- + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( + spatial_footprint + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def calc_rotation_matrix( + roll_ref: float, v3i_yangle: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yangle : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: list + A list containing the rotation matrix elements in column order. + + Notes + ----- + The rotation matrix is + + .. math:: + PC = \\begin{bmatrix} + pc_{1,1} & pc_{2,1} \\\\ + pc_{1,2} & pc_{2,2} + \\end{bmatrix} + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yangle) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + def wcs_from_footprints( dmodels, refmodel=None, @@ -597,7 +600,7 @@ def wcs_from_footprints( crval=None, ): """ - Create a WCS from a list of input data models. + Create a WCS from a list of input datamodels. A fiducial point in the output coordinate frame is created from the footprints of all WCS objects. For a spatial frame this is the center @@ -613,32 +616,31 @@ def wcs_from_footprints( Parameters ---------- - dmodels : list of valid datamodels - A list of data models. + dmodels : list + A list of valid datamodels. - refmodel : a valid datamodel, optional - This model's WCS is used as a reference. - WCS. The output coordinate frame, the projection and a - scaling and rotation transform is created from it. If not supplied - the first model in the list is used as ``refmodel``. + refmodel : + A valid datamodel whose WCS is used as reference for the creation of the output + coordinate frame, projection, and scaling and rotation transforms. + If not supplied the first model in the list is used as ``refmodel``. - transform : `~astropy.modeling.core.Model`, optional - A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` - If not supplied Scaling | Rotation is computed from ``refmodel``. + transform : ~astropy.modeling.Model + A transform, passed to :py:func:`gwcs.wcstools.wcs_from_fiducial` + If not supplied `Scaling | Rotation` is computed from ``refmodel``. - bounding_box : tuple, optional + bounding_box : tuple Bounding_box of the new WCS. If not supplied it is computed from the bounding_box of all inputs. - pscale_ratio : float, None, optional + pscale_ratio : float, None Ratio of input to output pixel scale. Ignored when either ``transform`` or ``pscale`` are provided. - pscale : float, None, optional + pscale : float, None Absolute pixel scale in degrees. When provided, overrides ``pscale_ratio``. Ignored when ``transform`` is provided. - rotation : float, None, optional + rotation : float, None Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. The default of `None` specifies that the images will not be rotated, @@ -647,24 +649,24 @@ def wcs_from_footprints( approximately to the detector axes. Ignored when ``transform`` is provided. - shape : tuple of int, None, optional + shape : tuple of int, None Shape of the image (data array) using ``numpy.ndarray`` convention (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - crpix : tuple of float, None, optional + crpix : tuple of float, None Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - crval : tuple of float, None, optional + crval : tuple of float, None Right ascension and declination of the reference pixel. Automatically computed if not provided. Returns ------- - wcs_new : `~gwcs.wcs.WCS` + wcs_new : ~gwcs.wcs.WCS The WCS object corresponding to the combined input footprints. """ From d52c60956cf119f7db84d8c1f1c41813fa7fdc1e Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 14 Jul 2023 12:34:41 -0400 Subject: [PATCH 033/117] Style check fix. --- src/stcal/alignment/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 39697617..26333f2e 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -567,7 +567,7 @@ def calc_rotation_matrix( Notes ----- The rotation matrix is - + .. math:: PC = \\begin{bmatrix} pc_{1,1} & pc_{2,1} \\\\ From 0a1fa458d1a9dd6b6ba5dcd392e258ec79509383 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 6 Jul 2023 16:49:24 -0400 Subject: [PATCH 034/117] Add methods necessary for resample. --- src/stcal/alignment/util.py | 420 ++++++++++++++++++++++++++++++++++++ tests/test_alignment.py | 144 +++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 tests/test_alignment.py diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index e69de29b..53f040b9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -0,0 +1,420 @@ +""" +Utility function for assign_wcs. + +""" +import logging +import functools +import numpy as np + +from astropy.coordinates import SkyCoord +from astropy.utils.misc import isiterable +from astropy import units as u +from astropy.modeling import models as astmodels +from typing import Union, List + +from gwcs import WCS +from gwcs import utils as gwutils +from gwcs.wcstools import wcs_from_fiducial + +from stdatamodels.jwst.datamodels import JwstDataModel +from roman_datamodels.datamodels import DataModel + + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +_MAX_SIP_DEGREE = 6 + + +__all__ = [ + "wcs_from_footprints", + "compute_scale", + "calc_rotation_matrix", +] + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : `~gwcs.wcs.WCS` + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def calc_rotation_matrix( + roll_ref: float, v3i_yang: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yang : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: [pc1_1, pc1_2, pc2_1, pc2_2] + The rotation matrix + + Notes + ----- + The rotation is + + ---------------- + | pc1_1 pc2_1 | + | pc1_2 pc2_2 | + ---------------- + + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yang) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + +def _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes +): + lon, lat = spatial_footprint + lon, lat = np.deg2rad(lon), np.deg2rad(lat) + x = np.cos(lat) * np.cos(lon) + y = np.cos(lat) * np.sin(lon) + z = np.sin(lat) + + x_mid = (np.max(x) + np.min(x)) / 2.0 + y_mid = (np.max(y) + np.min(y)) / 2.0 + z_mid = (np.max(z) + np.min(z)) / 2.0 + lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 + lat_fiducial = np.rad2deg( + np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) + ) + fiducial[spatial_axes] = lon_fiducial, lat_fiducial + + +def compute_fiducial(wcslist, bounding_box=None): + """ + For a celestial footprint this is the center. + For a spectral footprint, it is the beginning of the range. + + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + _calculate_fiducial_from_spatial_footprint( + spatial_footprint, fiducial, spatial_axes + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def wcsinfo_from_model(input_model): + """ + Create a dict {wcs_keyword: array_of_values} pairs from a data model. + + Parameters + ---------- + input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` + The input data model + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes)) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial +): + wcsinfo = wcsinfo_from_model(refmodel) + if isinstance(refmodel, JwstDataModel): + sky_axes, spec, other = gwutils.get_axes(wcsinfo) + elif isinstance(refmodel, DataModel): + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + + # Need to put the rotation matrix (List[float, float, float, float]) + # returned from calc_rotation_matrix into the correct shape for + # constructing the transformation + v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) + vparity = refmodel.meta.wcsinfo.vparity + if rotation is None: + roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + else: + roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + + pc = np.reshape( + calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + ) + + rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") + transform = [rotation] + if sky_axes: + if not pscale: + pscale = compute_scale( + refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio + ) + transform.append( + astmodels.Scale(pscale, name="cdelt1") + & astmodels.Scale(pscale, name="cdelt2") + ) + + if transform: + transform = functools.reduce(lambda x, y: x | y, transform) + return transform + + +def wcs_from_footprints( + dmodels, + refmodel=None, + transform=None, + bounding_box=None, + pscale_ratio=None, + pscale=None, + rotation=None, + shape=None, + crpix=None, + crval=None, +): + """ + Create a WCS from a list of input data models. + + A fiducial point in the output coordinate frame is created from the + footprints of all WCS objects. For a spatial frame this is the center + of the union of the footprints. For a spectral frame the fiducial is in + the beginning of the footprint range. + If ``refmodel`` is None, the first WCS object in the list is considered + a reference. The output coordinate frame and projection (for celestial frames) + is taken from ``refmodel``. + If ``transform`` is not supplied, a compound transform is created using + CDELTs and PC. + If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed + from bounding_box of all input WCSs. + + Parameters + ---------- + dmodels : list of `~jwst.datamodels.JwstDataModel` + A list of data models. + refmodel : `~jwst.datamodels.JwstDataModel`, optional + This model's WCS is used as a reference. + WCS. The output coordinate frame, the projection and a + scaling and rotation transform is created from it. If not supplied + the first model in the list is used as ``refmodel``. + transform : `~astropy.modeling.core.Model`, optional + A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` + If not supplied Scaling | Rotation is computed from ``refmodel``. + bounding_box : tuple, optional + Bounding_box of the new WCS. + If not supplied it is computed from the bounding_box of all inputs. + pscale_ratio : float, None, optional + Ratio of input to output pixel scale. Ignored when either + ``transform`` or ``pscale`` are provided. + pscale : float, None, optional + Absolute pixel scale in degrees. When provided, overrides + ``pscale_ratio``. Ignored when ``transform`` is provided. + rotation : float, None, optional + Position angle of output image's Y-axis relative to North. + A value of 0.0 would orient the final output image to be North up. + The default of `None` specifies that the images will not be rotated, + but will instead be resampled in the default orientation for the camera + with the x and y axes of the resampled image corresponding + approximately to the detector axes. Ignored when ``transform`` is + provided. + shape : tuple of int, None, optional + Shape of the image (data array) using ``numpy.ndarray`` convention + (``ny`` first and ``nx`` second). This value will be assigned to + ``pixel_shape`` and ``array_shape`` properties of the returned + WCS object. + crpix : tuple of float, None, optional + Position of the reference pixel in the image array. If ``crpix`` is not + specified, it will be set to the center of the bounding box of the + returned WCS object. + crval : tuple of float, None, optional + Right ascension and declination of the reference pixel. Automatically + computed if not provided. + + """ + bb = bounding_box + wcslist = [im.meta.wcs for im in dmodels] + + if not isiterable(wcslist): + raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") + + if not all(isinstance(w, WCS) for w in wcslist): + raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") + + if refmodel is None: + refmodel = dmodels[0] + elif not isinstance(refmodel, (JwstDataModel, DataModel)): + raise TypeError("Expected refmodel to be an instance of DataModel.") + + fiducial = compute_fiducial(wcslist, bb) + if crval is not None: + # overwrite spatial axes with user-provided CRVAL: + i = 0 + for k, axt in enumerate(wcslist[0].output_frame.axes_type): + if axt == "SPATIAL": + fiducial[k] = crval[i] + i += 1 + + ref_fiducial = np.array( + [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + ) + + prj = astmodels.Pix2Sky_TAN() + + if transform is None: + transform = _generate_tranform_from_datamodel( + refmodel, pscale_ratio, pscale, rotation, ref_fiducial + ) + + out_frame = refmodel.meta.wcs.output_frame + input_frame = refmodel.meta.wcs.input_frame + wnew = wcs_from_fiducial( + fiducial, + coordinate_frame=out_frame, + projection=prj, + transform=transform, + input_frame=input_frame, + ) + + footprints = [w.footprint().T for w in wcslist] + domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in out_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + if crpix is None: + offset1, offset2 = wnew.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] + else: + offset1, offset2 = crpix + offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" + ) + + wnew.insert_transform("detector", offsets, after=True) + wnew.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wnew.pixel_shape = shape[::-1] + wnew.array_shape = shape + + return wnew diff --git a/tests/test_alignment.py b/tests/test_alignment.py new file mode 100644 index 00000000..8e5aac4b --- /dev/null +++ b/tests/test_alignment.py @@ -0,0 +1,144 @@ +from astropy.modeling import models +from astropy import coordinates as coord +from astropy import units as u +from gwcs import WCS +from gwcs import coordinate_frames as cf +import numpy as np +import pytest +from stdatamodels.jwst.datamodels import ImageModel +from roman_datamodels.datamodels import DataModel +from stcal.alignment.util import ( + compute_fiducial, + compute_scale, + wcs_from_footprints, +) + + +def _create_wcs_object_without_distortion( + fiducial_world=(None, None), + pscale=None, + shape=None, +): + fiducial_detector = tuple(shape.value) + + # subtract 1 to account for pixel indexing starting at 0 + shift = models.Shift(-(fiducial_detector[0] - 1)) & models.Shift( + -(fiducial_detector[1] - 1) + ) + + scale = models.Scale(pscale[0].to("deg")) & models.Scale( + pscale[1].to("deg") + ) + + tan = models.Pix2Sky_TAN() + celestial_rotation = models.RotateNative2Celestial( + fiducial_world[0], + fiducial_world[1], + 180 * u.deg, + ) + + det2sky = shift | scale | tan | celestial_rotation + det2sky.name = "linear_transform" + + detector_frame = cf.Frame2D( + name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix) + ) + sky_frame = cf.CelestialFrame( + reference_frame=coord.FK5(), name="fk5", unit=(u.deg, u.deg) + ) + + pipeline = [(detector_frame, det2sky), (sky_frame, None)] + + wcs_obj = WCS(pipeline) + + wcs_obj.bounding_box = ( + (-0.5, fiducial_detector[0] - 0.5), + (-0.5, fiducial_detector[0] - 0.5), + ) + + return wcs_obj + + +def _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale): + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + datamodel = ImageModel(np.zeros(tuple(shape.value.astype(int)))) + datamodel.meta.wcs = wcs + datamodel.meta.wcsinfo.ra_ref = fiducial_world[0].value + datamodel.meta.wcsinfo.dec_ref = fiducial_world[1].value + datamodel.meta.wcsinfo.v2_ref = 0 + datamodel.meta.wcsinfo.v3_ref = 0 + datamodel.meta.wcsinfo.roll_ref = 0 + datamodel.meta.wcsinfo.v3yangle = 0 + datamodel.meta.wcsinfo.vparity = -1 + datamodel.meta.wcsinfo.wcsaxes = 2 + datamodel.meta.coordinates.reference_frame = "ICRS" + datamodel.meta.wcsinfo.ctype1 = "RA---TAN" + datamodel.meta.wcsinfo.ctype2 = "DEC--TAN" + + return (wcs, datamodel) + + +def test_compute_fiducial(): + """Test that util.compute_fiducial can properly determine the center of the + WCS's footprint. + """ + + shape = (3, 3) * u.pix + fiducial_world = (0, 0) * u.deg + pscale = (0.05, 0.05) * u.arcsec + + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + + computed_fiducial = compute_fiducial([wcs]) + + assert all(np.isclose(wcs(1, 1), computed_fiducial)) + + +@pytest.mark.parametrize("pscales", [(0.05, 0.05), (0.1, 0.05)]) +def test_compute_scale(pscales): + """Test that util.compute_scale can properly determine the pixel scale of a + WCS object. + """ + shape = (3, 3) * u.pix + fiducial_world = (0, 0) * u.deg + pscale = (pscales[0], pscales[1]) * u.arcsec + + wcs = _create_wcs_object_without_distortion( + fiducial_world=fiducial_world, shape=shape, pscale=pscale + ) + expected_scale = np.sqrt(pscale[0].to("deg") * pscale[1].to("deg")).value + + computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world.value) + + assert np.isclose(expected_scale, computed_scale) + + +def test_wcs_from_footprints(): + shape = (3, 3) * u.pix + fiducial_world = (10, 0) * u.deg + pscale = (0.1, 0.1) * u.arcsec + + wcs_1, dm_1 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + + # new fiducial will be shifted by one pixel in both directions + fiducial_world -= pscale + wcs_2, dm_2 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + + # check overlapping pixels have approximate the same world coordinate + assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) + assert all(np.isclose(wcs_1(1, 0), wcs_2(2, 1))) + assert all(np.isclose(wcs_1(0, 0), wcs_2(1, 1))) + assert all(np.isclose(wcs_1(1, 1), wcs_2(2, 2))) + + wcs = wcs_from_footprints([dm_1, dm_2]) + + # check that center of calculated WCS matches the + # expected position onto wcs_1 and wcs_2 + assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) + assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + + assert True From 846d6f0ce9e4754915a1fc079658b68f302eefbc Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:01:21 -0400 Subject: [PATCH 035/117] Small changes to accommodate both JWST and RST datamodels. --- src/stcal/alignment/util.py | 9 ++-- tests/test_alignment.py | 82 +++++++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 53f040b9..063db199 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -17,7 +17,7 @@ from gwcs.wcstools import wcs_from_fiducial from stdatamodels.jwst.datamodels import JwstDataModel -from roman_datamodels.datamodels import DataModel +from roman_datamodels.datamodels import DataModel as RstDataModel log = logging.getLogger(__name__) @@ -231,10 +231,11 @@ def wcsinfo_from_model(input_model): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - wcsinfo = wcsinfo_from_model(refmodel) if isinstance(refmodel, JwstDataModel): + wcsinfo = wcsinfo_from_model(refmodel) sky_axes, spec, other = gwutils.get_axes(wcsinfo) - elif isinstance(refmodel, DataModel): + elif isinstance(refmodel, RstDataModel): + wcsinfo = refmodel.meta.wcsinfo sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -349,7 +350,7 @@ def wcs_from_footprints( if refmodel is None: refmodel = dmodels[0] - elif not isinstance(refmodel, (JwstDataModel, DataModel)): + elif not isinstance(refmodel, (JwstDataModel, RstDataModel)): raise TypeError("Expected refmodel to be an instance of DataModel.") fiducial = compute_fiducial(wcslist, bb) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 8e5aac4b..3c2bb3fe 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -6,7 +6,8 @@ import numpy as np import pytest from stdatamodels.jwst.datamodels import ImageModel -from roman_datamodels.datamodels import DataModel +from roman_datamodels import datamodels as rdm +from roman_datamodels import maker_utils as utils from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -59,25 +60,57 @@ def _create_wcs_object_without_distortion( return wcs_obj -def _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale): +def _create_wcs_and_datamodel(datamodel_type, fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - datamodel = ImageModel(np.zeros(tuple(shape.value.astype(int)))) - datamodel.meta.wcs = wcs - datamodel.meta.wcsinfo.ra_ref = fiducial_world[0].value - datamodel.meta.wcsinfo.dec_ref = fiducial_world[1].value - datamodel.meta.wcsinfo.v2_ref = 0 - datamodel.meta.wcsinfo.v3_ref = 0 - datamodel.meta.wcsinfo.roll_ref = 0 - datamodel.meta.wcsinfo.v3yangle = 0 - datamodel.meta.wcsinfo.vparity = -1 - datamodel.meta.wcsinfo.wcsaxes = 2 - datamodel.meta.coordinates.reference_frame = "ICRS" - datamodel.meta.wcsinfo.ctype1 = "RA---TAN" - datamodel.meta.wcsinfo.ctype2 = "DEC--TAN" - - return (wcs, datamodel) + if datamodel_type == "jwst": + datamodel = _create_jwst_meta(shape, fiducial_world, wcs) + elif datamodel_type == "roman": + datamodel = _create_roman_meta(shape, fiducial_world, wcs) + + return datamodel + + +def _create_jwst_meta(shape, fiducial_world, wcs): + result = ImageModel(np.zeros(tuple(shape.value.astype(int)))) + + result.meta.wcsinfo.ra_ref = fiducial_world[0].value + result.meta.wcsinfo.dec_ref = fiducial_world[1].value + result.meta.wcsinfo.ctype1 = "RA---TAN" + result.meta.wcsinfo.ctype2 = "DEC--TAN" + result.meta.wcsinfo.v2_ref = 0 + result.meta.wcsinfo.v3_ref = 0 + result.meta.wcsinfo.roll_ref = 0 + result.meta.wcsinfo.v3yangle = 0 + result.meta.wcsinfo.vparity = -1 + result.meta.wcsinfo.wcsaxes = 2 + + result.meta.coordinates.reference_frame = "ICRS" + + result.meta.wcs = wcs + + return result + + +def _create_roman_meta(shape, fiducial_world, wcs): + result = utils.mk_level2_image(shape=tuple(shape.value.astype(int))) + + result.meta.wcsinfo.ra_ref = fiducial_world[0].value + result.meta.wcsinfo.dec_ref = fiducial_world[1].value + result.meta.wcsinfo.v2_ref = 0 + result.meta.wcsinfo.v3_ref = 0 + result.meta.wcsinfo.roll_ref = 0 + result.meta.wcsinfo.v3yangle = 0 + result.meta.wcsinfo.vparity = -1 + + result.meta.coordinates.reference_frame = "ICRS" + + result.meta["wcs"] = wcs + + result = rdm.ImageModel(result) + + return result def test_compute_fiducial(): @@ -117,16 +150,23 @@ def test_compute_scale(pscales): assert np.isclose(expected_scale, computed_scale) -def test_wcs_from_footprints(): +@pytest.mark.parametrize("datamodel_type", ["jwst", "roman"]) +def test_wcs_from_footprints(datamodel_type): shape = (3, 3) * u.pix fiducial_world = (10, 0) * u.deg pscale = (0.1, 0.1) * u.arcsec - wcs_1, dm_1 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + dm_1 = _create_wcs_and_datamodel( + datamodel_type, fiducial_world, shape, pscale + ) + wcs_1 = dm_1.meta.wcs # new fiducial will be shifted by one pixel in both directions fiducial_world -= pscale - wcs_2, dm_2 = _create_wcs_and_jwst_datamodel(fiducial_world, shape, pscale) + dm_2 = _create_wcs_and_datamodel( + datamodel_type, fiducial_world, shape, pscale + ) + wcs_2 = dm_2.meta.wcs # check overlapping pixels have approximate the same world coordinate assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) @@ -140,5 +180,3 @@ def test_wcs_from_footprints(): # expected position onto wcs_1 and wcs_2 assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) - - assert True From ec98251801d14f4cdf6f387bd8330200e80229b3 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:07:27 -0400 Subject: [PATCH 036/117] Add CHANGES.rst entry. --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8b8d5d07..d239c3fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +1.4.2 (unreleased) +================== +- Added ``alignment`` sub-package. + 1.4.1 (2023-06-29) ================== From d74b115b1375dba8a289f4e93d7852faadc00753 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:08:39 -0400 Subject: [PATCH 037/117] Add PR number to CHANGES.rst entry. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d239c3fa..030bd1f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,6 @@ 1.4.2 (unreleased) ================== -- Added ``alignment`` sub-package. +- Added ``alignment`` sub-package. [#179] 1.4.1 (2023-06-29) ================== From 57a3ef9f11214cedd79a9c8b93afbcabef10d918 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:50:16 -0400 Subject: [PATCH 038/117] use protocols --- src/stcal/alignment/util.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 063db199..3b074bde 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -4,21 +4,20 @@ """ import logging import functools +from typing import List, Protocol, Union + import numpy as np from astropy.coordinates import SkyCoord from astropy.utils.misc import isiterable from astropy import units as u from astropy.modeling import models as astmodels -from typing import Union, List +from asdf import AsdfFile from gwcs import WCS from gwcs import utils as gwutils from gwcs.wcstools import wcs_from_fiducial -from stdatamodels.jwst.datamodels import JwstDataModel -from roman_datamodels.datamodels import DataModel as RstDataModel - log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -34,6 +33,13 @@ ] +class SupportsDataWithWcs(Protocol): + _asdf: AsdfFile + + def to_flat_dict(): + ... + + def compute_scale( wcs: WCS, fiducial: Union[tuple, np.ndarray], @@ -189,7 +195,7 @@ def compute_fiducial(wcslist, bounding_box=None): return fiducial -def wcsinfo_from_model(input_model): +def wcsinfo_from_model(input_model: SupportsDataWithWcs): """ Create a dict {wcs_keyword: array_of_values} pairs from a data model. @@ -231,16 +237,12 @@ def wcsinfo_from_model(input_model): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - if isinstance(refmodel, JwstDataModel): - wcsinfo = wcsinfo_from_model(refmodel) - sky_axes, spec, other = gwutils.get_axes(wcsinfo) - elif isinstance(refmodel, RstDataModel): - wcsinfo = refmodel.meta.wcsinfo - sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - - # Need to put the rotation matrix (List[float, float, float, float]) - # returned from calc_rotation_matrix into the correct shape for - # constructing the transformation + wcsinfo = refmodel.meta.wcsinfo + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + + # Need to put the rotation matrix (List[float, float, float, float]) + # returned from calc_rotation_matrix into the correct shape for + # constructing the transformation v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) vparity = refmodel.meta.wcsinfo.vparity if rotation is None: From 727c7a166be04102ef79b1a217b7c5ac208e2a85 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:52:03 -0400 Subject: [PATCH 039/117] temp --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddf0a206..3d4bd018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main - '*x' + - '*' tags: - '*' pull_request: From 233ce462c320d2d2a0262f588b4435c85a2176ce Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 06:58:28 -0400 Subject: [PATCH 040/117] add dependencies --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 58c16764..b5656be0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,10 @@ dependencies = [ 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', + 'asdf', + 'gwcs', + 'stdatamodels', + 'roman_datamodels', ] dynamic = ['version'] From c723e32d5010864c063f11fb68558a6b7bc3c62b Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 17:49:23 -0400 Subject: [PATCH 041/117] add CI testing to alignment branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d4bd018..3657a391 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,12 @@ on: branches: - main - '*x' - - '*' tags: - '*' pull_request: branches: - main + - stcal-alignment schedule: # Weekly Monday 9AM build - cron: "0 9 * * 1" From da051fb0bb85c1aa6123f8edbeea30822823a557 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 17:51:08 -0400 Subject: [PATCH 042/117] add CI testing to alignment branch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3657a391..4d8a8e8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main - '*x' + - use-protocols tags: - '*' pull_request: From 520464f0df94670125502cdeb178250e6f50b8d5 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 20:08:15 -0400 Subject: [PATCH 043/117] fix test --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 - src/stcal/alignment/util.py | 4 -- tests/test_alignment.py | 92 +++++++++++++++---------------------- 4 files changed, 39 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d8a8e8a..c7e9e8f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - main - '*x' - - use-protocols + - stcal-alignment tags: - '*' pull_request: diff --git a/pyproject.toml b/pyproject.toml index b5656be0..9522b45c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,6 @@ dependencies = [ 'opencv-python-headless >=4.6.0.66', 'asdf', 'gwcs', - 'stdatamodels', - 'roman_datamodels', ] dynamic = ['version'] diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 3b074bde..4464bed9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -15,7 +15,6 @@ from asdf import AsdfFile from gwcs import WCS -from gwcs import utils as gwutils from gwcs.wcstools import wcs_from_fiducial @@ -237,7 +236,6 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): def _generate_tranform_from_datamodel( refmodel, pscale_ratio, pscale, rotation, ref_fiducial ): - wcsinfo = refmodel.meta.wcsinfo sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -352,8 +350,6 @@ def wcs_from_footprints( if refmodel is None: refmodel = dmodels[0] - elif not isinstance(refmodel, (JwstDataModel, RstDataModel)): - raise TypeError("Expected refmodel to be an instance of DataModel.") fiducial = compute_fiducial(wcslist, bb) if crval is not None: diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 3c2bb3fe..77d8a544 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -1,13 +1,14 @@ +import numpy as np + from astropy.modeling import models from astropy import coordinates as coord from astropy import units as u + from gwcs import WCS from gwcs import coordinate_frames as cf -import numpy as np + import pytest -from stdatamodels.jwst.datamodels import ImageModel -from roman_datamodels import datamodels as rdm -from roman_datamodels import maker_utils as utils + from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -16,9 +17,9 @@ def _create_wcs_object_without_distortion( - fiducial_world=(None, None), - pscale=None, - shape=None, + fiducial_world, + pscale, + shape, ): fiducial_detector = tuple(shape.value) @@ -60,57 +61,45 @@ def _create_wcs_object_without_distortion( return wcs_obj -def _create_wcs_and_datamodel(datamodel_type, fiducial_world, shape, pscale): +def _create_wcs_and_datamodel(fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - if datamodel_type == "jwst": - datamodel = _create_jwst_meta(shape, fiducial_world, wcs) - elif datamodel_type == "roman": - datamodel = _create_roman_meta(shape, fiducial_world, wcs) - + ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value + datamodel = DataModel(ra_ref=ra_ref, dec_ref=dec_ref, roll_ref=0, + v2_ref=0, v3_ref=0, v3yangle=0, wcs=wcs) return datamodel -def _create_jwst_meta(shape, fiducial_world, wcs): - result = ImageModel(np.zeros(tuple(shape.value.astype(int)))) +class WcsInfo: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle): + self.ra_ref = ra_ref + self.dec_ref = dec_ref + self.ctype1 = "RA---TAN" + self.ctype2 = "DEC--TAN" + self.v2_ref = v2_ref + self.v3_ref = v3_ref + self.v3yangle = v3yangle + self.roll_ref = roll_ref + self.vparity = -1 + self.wcsaxes = 2 - result.meta.wcsinfo.ra_ref = fiducial_world[0].value - result.meta.wcsinfo.dec_ref = fiducial_world[1].value - result.meta.wcsinfo.ctype1 = "RA---TAN" - result.meta.wcsinfo.ctype2 = "DEC--TAN" - result.meta.wcsinfo.v2_ref = 0 - result.meta.wcsinfo.v3_ref = 0 - result.meta.wcsinfo.roll_ref = 0 - result.meta.wcsinfo.v3yangle = 0 - result.meta.wcsinfo.vparity = -1 - result.meta.wcsinfo.wcsaxes = 2 - result.meta.coordinates.reference_frame = "ICRS" +class Coordinates: + def __init__(self): + self.reference_frame = "ICRS" - result.meta.wcs = wcs - return result +class MetaData: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): + self.wcsinfo = WcsInfo(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle) + self.wcs = wcs + self.coordinates=Coordinates() -def _create_roman_meta(shape, fiducial_world, wcs): - result = utils.mk_level2_image(shape=tuple(shape.value.astype(int))) - - result.meta.wcsinfo.ra_ref = fiducial_world[0].value - result.meta.wcsinfo.dec_ref = fiducial_world[1].value - result.meta.wcsinfo.v2_ref = 0 - result.meta.wcsinfo.v3_ref = 0 - result.meta.wcsinfo.roll_ref = 0 - result.meta.wcsinfo.v3yangle = 0 - result.meta.wcsinfo.vparity = -1 - - result.meta.coordinates.reference_frame = "ICRS" - - result.meta["wcs"] = wcs - - result = rdm.ImageModel(result) - - return result +class DataModel: + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): + self.meta = MetaData(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs) def test_compute_fiducial(): @@ -150,22 +139,17 @@ def test_compute_scale(pscales): assert np.isclose(expected_scale, computed_scale) -@pytest.mark.parametrize("datamodel_type", ["jwst", "roman"]) -def test_wcs_from_footprints(datamodel_type): +def test_wcs_from_footprints(): shape = (3, 3) * u.pix fiducial_world = (10, 0) * u.deg pscale = (0.1, 0.1) * u.arcsec - dm_1 = _create_wcs_and_datamodel( - datamodel_type, fiducial_world, shape, pscale - ) + dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs # new fiducial will be shifted by one pixel in both directions fiducial_world -= pscale - dm_2 = _create_wcs_and_datamodel( - datamodel_type, fiducial_world, shape, pscale - ) + dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs # check overlapping pixels have approximate the same world coordinate From 07912e00b2d7bb8132f6d6890fd6763a36518cff Mon Sep 17 00:00:00 2001 From: mwregan2 Date: Fri, 7 Jul 2023 14:21:00 -0400 Subject: [PATCH 044/117] Add setting of number_extended_events (#178) * Add setting of number_extended_events This was not being set when in single processing mode. * Update CHANGES.rst * Update CHANGES.rst * Update test_jump.py --------- Co-authored-by: Howard Bushouse --- CHANGES.rst | 11 +++++++++++ src/stcal/jump/jump.py | 6 +++++- tests/test_jump.py | 14 ++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 030bd1f9..c72120d9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,17 @@ Bug Fixes jump ~~~~ +- Added setting of number_extended_events for non-multiprocessing + mode. This is the value that is put into the header keyword EXTNCRS. [#178] + +1.4.1 (2023-06-29) + +Bug Fixes +--------- + +jump +~~~~ + - Added statement to prevent the number of cores used in multiprocessing from being larger than the number of rows. This was causing some CI tests to fail. [#176] diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 250d1ebe..078d58b0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -262,13 +262,15 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, only_use_ints=only_use_ints) # This is the flag that controls the flagging of either snowballs. if expand_large_events: - flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, + total_snowballs = flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, min_jump_area=min_jump_area, expand_factor=expand_factor, sat_required_snowball=sat_required_snowball, min_sat_radius_extend=min_sat_radius_extend, edge_size=edge_size, sat_expand=sat_expand, max_extended_radius=max_extended_radius) + log.info('Total snowballs = %i' % total_snowballs) + number_extended_events = total_snowballs if find_showers: gdq, num_showers = find_faint_extended(data, gdq, readnoise_2d, frames_per_group, minimum_sigclip_groups, @@ -280,6 +282,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, ellipse_expand=extend_ellipse_expand_ratio, num_grps_masked=grps_masked_after_shower, max_extended_radius=max_extended_radius) + log.info('Total showers= %i' % num_showers) + number_extended_events = num_showers else: yinc = int(n_rows / n_slices) slices = [] diff --git a/tests/test_jump.py b/tests/test_jump.py index ddfcbed1..49d65ba0 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -162,6 +162,7 @@ def test_find_faint_extended(): ellipse_expand=1.1, num_grps_masked=3) # Check that all the expected samples in group 2 are flagged as jump and # that they are not flagged outside + assert (num_showers == 3) assert (np.all(gdq[0, 1, 22, 14:23] == 0)) assert (np.all(gdq[0, 1, 21, 16:20] == DQFLAGS['JUMP_DET'])) assert (np.all(gdq[0, 1, 20, 15:22] == DQFLAGS['JUMP_DET'])) @@ -210,6 +211,7 @@ def test_find_faint_extended_sigclip(): ellipse_expand=1.1, num_grps_masked=3) # Check that all the expected samples in group 2 are flagged as jump and # that they are not flagged outside + assert(num_showers == 0) assert (np.all(gdq[0, 1, 22, 14:23] == 0)) assert (np.all(gdq[0, 1, 21, 16:20] == 0)) assert (np.all(gdq[0, 1, 20, 15:22] == 0)) @@ -265,10 +267,14 @@ def test_inputjumpall(): @pytest.mark.skip("Used for local testing") def test_inputjump_sat_star(): testcube = fits.getdata('data/input_gdq_flarge.fits') - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + num_extended_events = flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], + min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, + sat_required_snowball=True, + min_sat_radius_extend=2.5, + sat_expand=2) + assert(num_extended_events == 312) fits.writeto("outgdq2.fits", testcube, overwrite=True) From 22d46e92858f2a8f3526914af9c55d2f68d3e529 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Mon, 10 Jul 2023 11:18:06 -0400 Subject: [PATCH 045/117] Rename variables with FITS keywords names. --- src/stcal/alignment/util.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4464bed9..9e24b751 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -278,8 +278,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - crpix=None, - crval=None, + ref_pixel=None, + ref_coord=None, ): """ Create a WCS from a list of input data models. @@ -330,11 +330,11 @@ def wcs_from_footprints( (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - crpix : tuple of float, None, optional - Position of the reference pixel in the image array. If ``crpix`` is not + ref_pixel : tuple of float, None, optional + Position of the reference pixel in the image array. If ``ref_pixel`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - crval : tuple of float, None, optional + ref_coord : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. @@ -352,12 +352,12 @@ def wcs_from_footprints( refmodel = dmodels[0] fiducial = compute_fiducial(wcslist, bb) - if crval is not None: + if ref_coord is not None: # overwrite spatial axes with user-provided CRVAL: i = 0 for k, axt in enumerate(wcslist[0].output_frame.axes_type): if axt == "SPATIAL": - fiducial[k] = crval[i] + fiducial[k] = ref_coord[i] i += 1 ref_fiducial = np.array( @@ -395,14 +395,14 @@ def wcs_from_footprints( output_bounding_box.append((axis_min, axis_max)) output_bounding_box = tuple(output_bounding_box) - if crpix is None: + if ref_pixel is None: offset1, offset2 = wnew.backward_transform(*fiducial) offset1 -= axis_min_values[0] offset2 -= axis_min_values[1] else: - offset1, offset2 = crpix - offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" + offset1, offset2 = ref_pixel + offsets = astmodels.Shift(-offset1, name="ref_pixel1") & astmodels.Shift( + -offset2, name="ref_pixel2" ) wnew.insert_transform("detector", offsets, after=True) From 3f1f67715cacea310bed619c1631c18a52b11456 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Mon, 10 Jul 2023 11:21:54 -0400 Subject: [PATCH 046/117] Style reformating. --- tests/test_alignment.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 77d8a544..ce4c6863 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -66,9 +66,15 @@ def _create_wcs_and_datamodel(fiducial_world, shape, pscale): fiducial_world=fiducial_world, shape=shape, pscale=pscale ) ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value - datamodel = DataModel(ra_ref=ra_ref, dec_ref=dec_ref, roll_ref=0, - v2_ref=0, v3_ref=0, v3yangle=0, wcs=wcs) - return datamodel + return DataModel( + ra_ref=ra_ref, + dec_ref=dec_ref, + roll_ref=0, + v2_ref=0, + v3_ref=0, + v3yangle=0, + wcs=wcs, + ) class WcsInfo: @@ -91,15 +97,23 @@ def __init__(self): class MetaData: - def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): - self.wcsinfo = WcsInfo(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle) + def __init__( + self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None + ): + self.wcsinfo = WcsInfo( + ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle + ) self.wcs = wcs - self.coordinates=Coordinates() + self.coordinates = Coordinates() class DataModel: - def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): - self.meta = MetaData(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs) + def __init__( + self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None + ): + self.meta = MetaData( + ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs + ) def test_compute_fiducial(): From 66840c3a7b213198b3e394002349f5caeee8359a Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:34:52 -0400 Subject: [PATCH 047/117] Update docs to include stcal.alignment. --- docs/api.rst | 2 +- docs/conf.py | 18 +++++++++++++----- docs/stcal/alignment/description.rst | 4 ++++ docs/stcal/alignment/index.rst | 12 ++++++++++++ docs/stcal/package_index.rst | 1 + 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 docs/stcal/alignment/description.rst create mode 100644 docs/stcal/alignment/index.rst diff --git a/docs/api.rst b/docs/api.rst index 95659c47..a23b1894 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,4 +1,4 @@ -stcall API +stcal API ========== .. automodapi:: stcal diff --git a/docs/conf.py b/docs/conf.py index fbadfd5e..d0fe5754 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,11 +4,12 @@ from pathlib import Path import stsci_rtd_theme + if sys.version_info < (3, 11): import tomli as tomllib else: import tomllib - + def setup(app): try: @@ -27,7 +28,7 @@ def setup(app): # values here: with open(REPO_ROOT / "pyproject.toml", "rb") as configuration_file: conf = tomllib.load(configuration_file) -setup_metadata = conf['project'] +setup_metadata = conf["project"] project = setup_metadata["name"] primary_author = setup_metadata["authors"][0] @@ -38,8 +39,17 @@ def setup(app): version = package.__version__.split("-", 1)[0] release = package.__version__ +# Configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/devdocs", None), + "scipy": ("http://scipy.github.io/devdocs", None), + "matplotlib": ("http://matplotlib.org/", None), +} + extensions = [ "sphinx_automodapi.automodapi", + "sphinx.ext.intersphinx", "numpydoc", ] @@ -48,9 +58,7 @@ def setup(app): autoclass_content = "both" html_theme = "stsci_rtd_theme" -html_theme_options = { - "collapse_navigation": True -} +html_theme_options = {"collapse_navigation": True} html_theme_path = [stsci_rtd_theme.get_html_theme_path()] html_domain_indices = True html_sidebars = {"**": ["globaltoc.html", "relations.html", "searchbox.html"]} diff --git a/docs/stcal/alignment/description.rst b/docs/stcal/alignment/description.rst new file mode 100644 index 00000000..a537e476 --- /dev/null +++ b/docs/stcal/alignment/description.rst @@ -0,0 +1,4 @@ +Description +============ + +This sub-package contains all the modules common to all missions. \ No newline at end of file diff --git a/docs/stcal/alignment/index.rst b/docs/stcal/alignment/index.rst new file mode 100644 index 00000000..e8d65068 --- /dev/null +++ b/docs/stcal/alignment/index.rst @@ -0,0 +1,12 @@ +.. _alignment: + +=============== +Alignment Utils +=============== + +.. toctree:: + :maxdepth: 2 + + description.rst + +.. automodapi:: stcal.alignment diff --git a/docs/stcal/package_index.rst b/docs/stcal/package_index.rst index 44295cd1..b68f11b5 100644 --- a/docs/stcal/package_index.rst +++ b/docs/stcal/package_index.rst @@ -6,3 +6,4 @@ Package Index jump/index.rst ramp_fitting/index.rst + alignment/index.rst \ No newline at end of file From dddbea6d7aae5e9f7428319d520c27c148818529 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:50:34 -0400 Subject: [PATCH 048/117] Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. --- src/stcal/alignment/util.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 9e24b751..4464bed9 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -278,8 +278,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - ref_pixel=None, - ref_coord=None, + crpix=None, + crval=None, ): """ Create a WCS from a list of input data models. @@ -330,11 +330,11 @@ def wcs_from_footprints( (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - ref_pixel : tuple of float, None, optional - Position of the reference pixel in the image array. If ``ref_pixel`` is not + crpix : tuple of float, None, optional + Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - ref_coord : tuple of float, None, optional + crval : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. @@ -352,12 +352,12 @@ def wcs_from_footprints( refmodel = dmodels[0] fiducial = compute_fiducial(wcslist, bb) - if ref_coord is not None: + if crval is not None: # overwrite spatial axes with user-provided CRVAL: i = 0 for k, axt in enumerate(wcslist[0].output_frame.axes_type): if axt == "SPATIAL": - fiducial[k] = ref_coord[i] + fiducial[k] = crval[i] i += 1 ref_fiducial = np.array( @@ -395,14 +395,14 @@ def wcs_from_footprints( output_bounding_box.append((axis_min, axis_max)) output_bounding_box = tuple(output_bounding_box) - if ref_pixel is None: + if crpix is None: offset1, offset2 = wnew.backward_transform(*fiducial) offset1 -= axis_min_values[0] offset2 -= axis_min_values[1] else: - offset1, offset2 = ref_pixel - offsets = astmodels.Shift(-offset1, name="ref_pixel1") & astmodels.Shift( - -offset2, name="ref_pixel2" + offset1, offset2 = crpix + offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" ) wnew.insert_transform("detector", offsets, after=True) From 2c0d44ec4208936feabb4f2a724a25e21c65c9d5 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:55:12 -0400 Subject: [PATCH 049/117] Updates to address most comments. --- src/stcal/alignment/__init__.py | 1 + src/stcal/alignment/util.py | 140 +++++++++++++++++++++++++------- tests/test_alignment.py | 51 ++++++------ 3 files changed, 137 insertions(+), 55 deletions(-) diff --git a/src/stcal/alignment/__init__.py b/src/stcal/alignment/__init__.py index e69de29b..46d3a156 100644 --- a/src/stcal/alignment/__init__.py +++ b/src/stcal/alignment/__init__.py @@ -0,0 +1 @@ +from .util import * diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4464bed9..134b0e65 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -1,5 +1,5 @@ """ -Utility function for assign_wcs. +Common utility functions for datamodel alignment. """ import logging @@ -22,12 +22,10 @@ log.setLevel(logging.DEBUG) -_MAX_SIP_DEGREE = 6 - - __all__ = [ "wcs_from_footprints", "compute_scale", + "compute_fiducial", "calc_rotation_matrix", ] @@ -53,10 +51,12 @@ def compute_scale( Reference WCS object from which to compute a scaling factor. fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating reference points. + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating + reference points. disp_axis : int - Dispersion axis integer. Assumes the same convention as `wcsinfo.dispersion_direction` + Dispersion axis integer. Assumes the same convention as + `wcsinfo.dispersion_direction` pscale_ratio : int Ratio of input to output pixel scale @@ -104,7 +104,7 @@ def compute_scale( def calc_rotation_matrix( - roll_ref: float, v3i_yang: float, vparity: int = 1 + roll_ref: float, v3i_yangle: float, vparity: int = 1 ) -> List[float]: """Calculate the rotation matrix. @@ -113,7 +113,7 @@ def calc_rotation_matrix( roll_ref : float Telescope roll angle of V3 North over East at the ref. point in radians - v3i_yang : float + v3i_yangle : float The angle between ideal Y-axis and V3 in radians. vparity : int @@ -128,17 +128,14 @@ def calc_rotation_matrix( Notes ----- The rotation is + pc1_1 | pc2_1 - ---------------- - | pc1_1 pc2_1 | - | pc1_2 pc2_2 | - ---------------- - + pc1_2 | pc2_2 """ if vparity not in (1, -1): raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - rel_angle = roll_ref - (vparity * v3i_yang) + rel_angle = roll_ref - (vparity * v3i_yangle) pc1_1 = vparity * np.cos(rel_angle) pc1_2 = np.sin(rel_angle) @@ -149,8 +146,22 @@ def calc_rotation_matrix( def _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes -): + spatial_footprint: np.ndarray, +) -> np.ndarray: + """ + Calculates the fiducial coordinates from a given spatial footprint. + + Parameters + ---------- + spatial_footprint : `~numpy.ndarray` + A 2xN array containing the world coordinates of the WCS footprint's + bounding box, where N is the number of bounding box positions. + + Returns + ------- + lon_fiducial, lat_fiducial : `numpy.ndarray`, `numpy.ndarray` + The world coordinates of the fiducial point in the output coordinate frame. + """ lon, lat = spatial_footprint lon, lat = np.deg2rad(lon), np.deg2rad(lat) x = np.cos(lat) * np.cos(lon) @@ -164,14 +175,34 @@ def _calculate_fiducial_from_spatial_footprint( lat_fiducial = np.rad2deg( np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) ) - fiducial[spatial_axes] = lon_fiducial, lat_fiducial + return lon_fiducial, lat_fiducial -def compute_fiducial(wcslist, bounding_box=None): +def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: """ - For a celestial footprint this is the center. - For a spectral footprint, it is the beginning of the range. + Calculates the world coordinates of the fiducial point of a list of WCS objects. + For a celestial footprint this is the center. For a spectral footprint, it is the + beginning of its range. + + Parameters + ---------- + wcslist : list + A list containing all the WCS objects for which the fiducial is to be + calculated. + + bounding_box : `~astropy.modeling.bounding_box` or list, optional + The bounding box to be used when calculating the fiducial. + If a list is provided, it should be in the following format: + [[x0_lower, x0_upper], [x1_lower, x1_upper]]. + Returns + ------- + fiducial : `numpy.ndarray` + A two-elements array containing the world coordinates of the fiducial point + in the combined output coordinate frame. + + Notes + ----- This function assumes all WCSs have the same output coordinate frame. """ @@ -186,8 +217,8 @@ def compute_fiducial(wcslist, bounding_box=None): fiducial = np.empty(len(axes_types)) if spatial_footprint.any(): - _calculate_fiducial_from_spatial_footprint( - spatial_footprint, fiducial, spatial_axes + fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( + spatial_footprint ) if spectral_footprint.any(): fiducial[spectral_axes] = spectral_footprint.min() @@ -196,12 +227,17 @@ def compute_fiducial(wcslist, bounding_box=None): def wcsinfo_from_model(input_model: SupportsDataWithWcs): """ - Create a dict {wcs_keyword: array_of_values} pairs from a data model. + Creates a dict {wcs_keyword: array_of_values} pairs from a data model. Parameters ---------- input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` - The input data model + The input data model. + + Returns + ------- + wcsinfo : dict + A dict containing the WCS FITS keywords and corresponding values. """ defaults = { @@ -221,7 +257,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): val.append(v) wcsinfo[key] = np.array(val) - pc = np.zeros((wcsaxes, wcsaxes)) + pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) for i in range(1, wcsaxes + 1): for j in range(1, wcsaxes + 1): pc[i - 1, j - 1] = getattr( @@ -234,8 +270,45 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): def _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial + refmodel: SupportsDataWithWcs, + ref_fiducial: np.array, + pscale_ratio: int = None, + pscale: float = None, + rotation: float = None, ): + """ + Creates a transform from pixel to world coordinates based on a + reference datamodel's WCS. + + Parameters + ---------- + refmodel : a valid datamodel + The datamodel that should be used as reference for calculating the + transform parameters. + pscale_ratio : int, None, optional + Ratio of input to output pixel scale. This parameter is only used when + pscale=`None` and, in that case, it is passed on to `compute_scale`. + pscale : float, None, optional + The plate scale. If `None`, the plate scale is calculated from the reference + datamodel. + rotation : float, None, optional + Position angle of output image's Y-axis relative to North. + A value of 0.0 would orient the final output image to be North up. + The default of `None` specifies that the images will not be rotated, + but will instead be resampled in the default orientation for the camera + with the x and y axes of the resampled image corresponding + approximately to the detector axes. Ignored when ``transform`` is + provided. If `None`, the rotation angle is extracted from the + reference model's `meta.wcsinfo.roll_ref`. + ref_fiducial : np.array + A two-elements array containing the world coordinates of the fiducial point. + + Returns + ------- + transform : `~astropy.modeling.core.CompoundModel` + An `~astropy.modeling` compound model containing the transform from pixel to + world coordinates. + """ sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() # Need to put the rotation matrix (List[float, float, float, float]) @@ -298,9 +371,9 @@ def wcs_from_footprints( Parameters ---------- - dmodels : list of `~jwst.datamodels.JwstDataModel` + dmodels : list of valid datamodels A list of data models. - refmodel : `~jwst.datamodels.JwstDataModel`, optional + refmodel : a valid datamodel, optional This model's WCS is used as a reference. WCS. The output coordinate frame, the projection and a scaling and rotation transform is created from it. If not supplied @@ -338,6 +411,11 @@ def wcs_from_footprints( Right ascension and declination of the reference pixel. Automatically computed if not provided. + Returns + ------- + wnew : `~gwcs.WCS` + The WCS object associated with the combined input footprints. + """ bb = bounding_box wcslist = [im.meta.wcs for im in dmodels] @@ -368,7 +446,11 @@ def wcs_from_footprints( if transform is None: transform = _generate_tranform_from_datamodel( - refmodel, pscale_ratio, pscale, rotation, ref_fiducial + refmodel=refmodel, + pscale_ratio=pscale_ratio, + pscale=pscale, + rotation=rotation, + ref_fiducial=ref_fiducial, ) out_frame = refmodel.meta.wcs.output_frame diff --git a/tests/test_alignment.py b/tests/test_alignment.py index ce4c6863..7aaba7aa 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -21,22 +21,16 @@ def _create_wcs_object_without_distortion( pscale, shape, ): - fiducial_detector = tuple(shape.value) - # subtract 1 to account for pixel indexing starting at 0 - shift = models.Shift(-(fiducial_detector[0] - 1)) & models.Shift( - -(fiducial_detector[1] - 1) - ) + shift = models.Shift(-(shape[0] - 1)) & models.Shift(-(shape[1] - 1)) - scale = models.Scale(pscale[0].to("deg")) & models.Scale( - pscale[1].to("deg") - ) + scale = models.Scale(pscale[0]) & models.Scale(pscale[1]) tan = models.Pix2Sky_TAN() celestial_rotation = models.RotateNative2Celestial( fiducial_world[0], fiducial_world[1], - 180 * u.deg, + 180, ) det2sky = shift | scale | tan | celestial_rotation @@ -54,8 +48,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (-0.5, fiducial_detector[0] - 0.5), - (-0.5, fiducial_detector[0] - 0.5), + (-0.5, shape[0] - 0.5), + (-0.5, shape[0] - 0.5), ) return wcs_obj @@ -65,7 +59,7 @@ def _create_wcs_and_datamodel(fiducial_world, shape, pscale): wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - ra_ref, dec_ref = fiducial_world[0].value, fiducial_world[1].value + ra_ref, dec_ref = fiducial_world[0], fiducial_world[1] return DataModel( ra_ref=ra_ref, dec_ref=dec_ref, @@ -121,9 +115,9 @@ def test_compute_fiducial(): WCS's footprint. """ - shape = (3, 3) * u.pix - fiducial_world = (0, 0) * u.deg - pscale = (0.05, 0.05) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (0, 0) # in deg + pscale = (0.000014, 0.000014) # in deg/pixel wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale @@ -134,35 +128,40 @@ def test_compute_fiducial(): assert all(np.isclose(wcs(1, 1), computed_fiducial)) -@pytest.mark.parametrize("pscales", [(0.05, 0.05), (0.1, 0.05)]) +@pytest.mark.parametrize( + "pscales", [(0.000014, 0.000014), (0.000028, 0.000014)] +) def test_compute_scale(pscales): """Test that util.compute_scale can properly determine the pixel scale of a WCS object. """ - shape = (3, 3) * u.pix - fiducial_world = (0, 0) * u.deg - pscale = (pscales[0], pscales[1]) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (0, 0) # in deg + pscale = (pscales[0], pscales[1]) # in deg/pixel wcs = _create_wcs_object_without_distortion( fiducial_world=fiducial_world, shape=shape, pscale=pscale ) - expected_scale = np.sqrt(pscale[0].to("deg") * pscale[1].to("deg")).value + expected_scale = np.sqrt(pscale[0] * pscale[1]) - computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world.value) + computed_scale = compute_scale(wcs=wcs, fiducial=fiducial_world) assert np.isclose(expected_scale, computed_scale) def test_wcs_from_footprints(): - shape = (3, 3) * u.pix - fiducial_world = (10, 0) * u.deg - pscale = (0.1, 0.1) * u.arcsec + shape = (3, 3) # in pixels + fiducial_world = (10, 0) # in deg + pscale = (0.000028, 0.000028) # in deg/pixel dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs - # new fiducial will be shifted by one pixel in both directions - fiducial_world -= pscale + # shift fiducial by one pixel in both directions and create a new WCS + fiducial_world = ( + fiducial_world[0] - 0.000028, + fiducial_world[1] - 0.000028, + ) dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs From 376f7febe31908ae164591fbde98447ad5e7e03a Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 11:26:08 -0400 Subject: [PATCH 050/117] Fix misplaced comment. --- src/stcal/alignment/util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 134b0e65..1108692c 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -310,10 +310,6 @@ def _generate_tranform_from_datamodel( world coordinates. """ sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - - # Need to put the rotation matrix (List[float, float, float, float]) - # returned from calc_rotation_matrix into the correct shape for - # constructing the transformation v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) vparity = refmodel.meta.wcsinfo.vparity if rotation is None: @@ -321,6 +317,8 @@ def _generate_tranform_from_datamodel( else: roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + # reshape the rotation matrix returned from calc_rotation_matrix + # into the correct shape for constructing the transformation pc = np.reshape( calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) ) From d78b736ec40da50394d1d1d36299e71cf766098a Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 11:39:23 -0400 Subject: [PATCH 051/117] Code refactoring and additional unit test. --- src/stcal/alignment/util.py | 415 ++++++++++++++++++++++++++---------- tests/test_alignment.py | 41 ++++ 2 files changed, 349 insertions(+), 107 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 1108692c..a2df71a4 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -190,10 +190,12 @@ def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: A list containing all the WCS objects for which the fiducial is to be calculated. - bounding_box : `~astropy.modeling.bounding_box` or list, optional - The bounding box to be used when calculating the fiducial. - If a list is provided, it should be in the following format: - [[x0_lower, x0_upper], [x1_lower, x1_upper]]. + bounding_box : tuple, or list, optional + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. Returns ------- @@ -269,12 +271,13 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): return wcsinfo -def _generate_tranform_from_datamodel( +def _generate_tranform( refmodel: SupportsDataWithWcs, ref_fiducial: np.array, pscale_ratio: int = None, pscale: float = None, rotation: float = None, + transform=None, ): """ Creates a transform from pixel to world coordinates based on a @@ -285,12 +288,15 @@ def _generate_tranform_from_datamodel( refmodel : a valid datamodel The datamodel that should be used as reference for calculating the transform parameters. + pscale_ratio : int, None, optional Ratio of input to output pixel scale. This parameter is only used when pscale=`None` and, in that case, it is passed on to `compute_scale`. + pscale : float, None, optional The plate scale. If `None`, the plate scale is calculated from the reference datamodel. + rotation : float, None, optional Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. @@ -300,44 +306,282 @@ def _generate_tranform_from_datamodel( approximately to the detector axes. Ignored when ``transform`` is provided. If `None`, the rotation angle is extracted from the reference model's `meta.wcsinfo.roll_ref`. + ref_fiducial : np.array A two-elements array containing the world coordinates of the fiducial point. + transform : `~astropy.modeling.Model`, optional + A transform between frames. + + Returns + ------- + transform : `~astropy.modeling.Model` + An `~astropy` model containing the transform between frames. + """ + if transform is None: + sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() + v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) + vparity = refmodel.meta.wcsinfo.vparity + if rotation is None: + roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + else: + roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + + # reshape the rotation matrix returned from calc_rotation_matrix + # into the correct shape for constructing the transformation + pc = np.reshape( + calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + ) + + rotation = astmodels.AffineTransformation2D( + pc, name="pc_rotation_matrix" + ) + transform = [rotation] + if sky_axes: + if not pscale: + pscale = compute_scale( + refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio + ) + transform.append( + astmodels.Scale(pscale, name="cdelt1") + & astmodels.Scale(pscale, name="cdelt2") + ) + + if transform: + transform = functools.reduce(lambda x, y: x | y, transform) + + return transform + + +def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): + """ + Calculates axis mininum values and bounding box. + + Parameters + ---------- + ref_model : a valid datamodel + The reference datamodel for which to determine the minimum axis values and + bounding box. + + wcs_list : list + The list of WCS objects. + + ref_wcs : `~gwcs.wcs.WCS` + The reference WCS object. + + Returns + ------- + tuple + A tuple containing two elements: + 1 - a `numpy.array` with the minimum value in each axis; + 2 - a tuple containing the bounding box region in the format + ((x0_lower, x0_upper), (x1_lower, x1_upper)). + """ + footprints = [w.footprint().T for w in wcs_list] + domain_bounds = np.hstack( + [ref_wcs.backward_transform(*f) for f in footprints] + ) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in ref_model.meta.wcs.output_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + # populate output_bounding_box + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + return (axis_min_values, output_bounding_box) + + +def _calculate_fiducial(wcs_list, bounding_box, crval=None): + """ + Calculates the coordinates of the fiducial point and, if necessary, updates it with + the values in CRVAL (the update is applied to spatial axes only). + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + bounding_box : tuple, or list, optional + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + crval : list, optional + A reference world coordinate associated with the reference pixel. If not `None`, + then the fiducial coordinates of the spatial axes will be updated with the + values from `crval`. + Returns ------- - transform : `~astropy.modeling.core.CompoundModel` - An `~astropy.modeling` compound model containing the transform from pixel to - world coordinates. + fiducial : `~numpy.ndarray` + A two-elements array containing the world coordinate of the fiducial point. """ - sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() - v3yangle = np.deg2rad(refmodel.meta.wcsinfo.v3yangle) - vparity = refmodel.meta.wcsinfo.vparity - if rotation is None: - roll_ref = np.deg2rad(refmodel.meta.wcsinfo.roll_ref) + fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) + if crval is not None: + i = 0 + for k, axt in enumerate(wcs_list[0].output_frame.axes_type): + if axt == "SPATIAL": + # overwrite only spatial axes with user-provided CRVAL + fiducial[k] = crval[i] + i += 1 + return fiducial + + +def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): + """ + Calculates the offsets to the transform. + + Parameters + ---------- + fiducial : `~numpy.ndarray` + A two-elements containing the world coordinates of the fiducial point. + + wcs : `~gwcs.wcs.WCS` + A WCS object. It will be used to determine the + + axis_min_values : `~numpy.ndarray` + A two-elements array containing the minimum pixel value for each axis. + + crpix : list or tuple + Pixel coordinates of the reference pixel. + + Returns + ------- + `~astropy.modeling.Model` + A model with the offsets to be added to the WCS's transform. + + Notes + ----- + If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. + The reason being that, in this case, the offsets will be calculated using the + WCS object to find the pixel coordinates of the fiducial point and then correct it + by the minimum pixel value for each axis. + """ + if ( + crpix is None + and fiducial is not None + and wcs is not None + and axis_min_values is not None + ): + offset1, offset2 = wcs.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] else: - roll_ref = np.deg2rad(rotation) + (vparity * v3yangle) + offset1, offset2 = crpix - # reshape the rotation matrix returned from calc_rotation_matrix - # into the correct shape for constructing the transformation - pc = np.reshape( - calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) + return astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" ) - rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") - transform = [rotation] - if sky_axes: - if not pscale: - pscale = compute_scale( - refmodel.meta.wcs, ref_fiducial, pscale_ratio=pscale_ratio - ) - transform.append( - astmodels.Scale(pscale, name="cdelt1") - & astmodels.Scale(pscale, name="cdelt2") + +def _calculate_new_wcs( + ref_model, shape, wcs_list, fiducial, crpix=None, transform=None +): + """ + Calculates a new WCS object based on the combined WCS objects provided. + + Parameters + ---------- + ref_model : a valid datamodel + The reference model to be used when extracting metadata. + + shape : list + The shape of the new WCS's pixel grid. If `None`, then the output bounding box + will be used to determine it. + + wcs_list : list + A list containing WCS objects. + + fiducial : `~numpy.ndarray` + A two-elements array containing the location on the sky in some standard + coordinate system. + + crpix : tuple, optional + The coordinates of the reference pixel. + + transform : `~astropy.modeling.Model`, optional + An optional tranform to be prepended to the transform constructed by the + fiducial point. The number of outputs of this transform must equal the number + of axes in the coordinate frame. + + Returns + ------- + `~gwcs.wcs.WCS` + The new WCS object that corresponds to the combined WCS objects in `wcs_list`. + """ + wcs_new = wcs_from_fiducial( + fiducial, + coordinate_frame=ref_model.meta.wcs.output_frame, + projection=astmodels.Pix2Sky_TAN(), + transform=transform, + input_frame=ref_model.meta.wcs.input_frame, + ) + axis_min_values, output_bounding_box = _get_axis_min_and_bounding_box( + ref_model, wcs_list, wcs_new + ) + offsets = _calculate_offsets( + fiducial=fiducial, + wcs=wcs_new, + axis_min_values=axis_min_values, + crpix=crpix, + ) + + wcs_new.insert_transform("detector", offsets, after=True) + wcs_new.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wcs_new.pixel_shape = shape[::-1] + wcs_new.array_shape = shape + return wcs_new + + +def _validate_wcs_list(wcs_list): + """ + Validates wcs_list. + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + Returns + ------- + bool or Exception + If wcs_list is valid, returns True. Otherwise, it will raise an error. + + Raises + ------ + ValueError + Raised whenever wcs_list is not an iterable. + TypeError + Raised whenever wcs_list is empty or any of its content is not an + instance of WCS. + """ + if not isiterable(wcs_list): + raise ValueError( + "Expected 'wcs_list' to be an iterable of WCS objects." ) + elif len(wcs_list): + if not all(isinstance(w, WCS) for w in wcs_list): + raise TypeError( + "All items in 'wcs_list' are to be instances of gwcs.WCS." + ) + else: + raise TypeError("'wcs_list' should not be empty.") - if transform: - transform = functools.reduce(lambda x, y: x | y, transform) - return transform + return True def wcs_from_footprints( @@ -364,30 +608,36 @@ def wcs_from_footprints( is taken from ``refmodel``. If ``transform`` is not supplied, a compound transform is created using CDELTs and PC. - If ``bounding_box`` is not supplied, the bounding_box of the new WCS is computed - from bounding_box of all input WCSs. + If ``bounding_box`` is not supplied, the `bounding_box` of the new WCS is computed + from `bounding_box` of all input WCSs. Parameters ---------- dmodels : list of valid datamodels A list of data models. + refmodel : a valid datamodel, optional This model's WCS is used as a reference. WCS. The output coordinate frame, the projection and a scaling and rotation transform is created from it. If not supplied the first model in the list is used as ``refmodel``. + transform : `~astropy.modeling.core.Model`, optional A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` If not supplied Scaling | Rotation is computed from ``refmodel``. + bounding_box : tuple, optional Bounding_box of the new WCS. If not supplied it is computed from the bounding_box of all inputs. + pscale_ratio : float, None, optional Ratio of input to output pixel scale. Ignored when either ``transform`` or ``pscale`` are provided. + pscale : float, None, optional Absolute pixel scale in degrees. When provided, overrides ``pscale_ratio``. Ignored when ``transform`` is provided. + rotation : float, None, optional Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. @@ -396,104 +646,55 @@ def wcs_from_footprints( with the x and y axes of the resampled image corresponding approximately to the detector axes. Ignored when ``transform`` is provided. + shape : tuple of int, None, optional Shape of the image (data array) using ``numpy.ndarray`` convention (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. + crpix : tuple of float, None, optional Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. + crval : tuple of float, None, optional Right ascension and declination of the reference pixel. Automatically computed if not provided. Returns ------- - wnew : `~gwcs.WCS` - The WCS object associated with the combined input footprints. + wcs_new : `~gwcs.wcs.WCS` + The WCS object corresponding to the combined input footprints. """ - bb = bounding_box - wcslist = [im.meta.wcs for im in dmodels] - - if not isiterable(wcslist): - raise ValueError("Expected 'wcslist' to be an iterable of WCS objects.") - if not all(isinstance(w, WCS) for w in wcslist): - raise TypeError("All items in wcslist are to be instances of gwcs.WCS.") + wcs_list = [im.meta.wcs for im in dmodels] - if refmodel is None: - refmodel = dmodels[0] - - fiducial = compute_fiducial(wcslist, bb) - if crval is not None: - # overwrite spatial axes with user-provided CRVAL: - i = 0 - for k, axt in enumerate(wcslist[0].output_frame.axes_type): - if axt == "SPATIAL": - fiducial[k] = crval[i] - i += 1 + _validate_wcs_list(wcs_list) - ref_fiducial = np.array( - [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + fiducial = _calculate_fiducial( + wcs_list=wcs_list, bounding_box=bounding_box, crval=crval ) - prj = astmodels.Pix2Sky_TAN() + refmodel = dmodels[0] if refmodel is None else refmodel - if transform is None: - transform = _generate_tranform_from_datamodel( - refmodel=refmodel, - pscale_ratio=pscale_ratio, - pscale=pscale, - rotation=rotation, - ref_fiducial=ref_fiducial, - ) - - out_frame = refmodel.meta.wcs.output_frame - input_frame = refmodel.meta.wcs.input_frame - wnew = wcs_from_fiducial( - fiducial, - coordinate_frame=out_frame, - projection=prj, + transform = _generate_tranform( + refmodel=refmodel, + pscale_ratio=pscale_ratio, + pscale=pscale, + rotation=rotation, + ref_fiducial=np.array( + [refmodel.meta.wcsinfo.ra_ref, refmodel.meta.wcsinfo.dec_ref] + ), transform=transform, - input_frame=input_frame, ) - footprints = [w.footprint().T for w in wcslist] - domain_bounds = np.hstack([wnew.backward_transform(*f) for f in footprints]) - axis_min_values = np.min(domain_bounds, axis=1) - domain_bounds = (domain_bounds.T - axis_min_values).T - - output_bounding_box = [] - for axis in out_frame.axes_order: - axis_min, axis_max = ( - domain_bounds[axis].min(), - domain_bounds[axis].max(), - ) - output_bounding_box.append((axis_min, axis_max)) - - output_bounding_box = tuple(output_bounding_box) - if crpix is None: - offset1, offset2 = wnew.backward_transform(*fiducial) - offset1 -= axis_min_values[0] - offset2 -= axis_min_values[1] - else: - offset1, offset2 = crpix - offsets = astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" + return _calculate_new_wcs( + ref_model=refmodel, + shape=shape, + crpix=crpix, + wcs_list=wcs_list, + fiducial=fiducial, + transform=transform, ) - - wnew.insert_transform("detector", offsets, after=True) - wnew.bounding_box = output_bounding_box - - if shape is None: - shape = [ - int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] - ] - - wnew.pixel_shape = shape[::-1] - wnew.array_shape = shape - - return wnew diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 7aaba7aa..70138581 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -13,6 +13,7 @@ compute_fiducial, compute_scale, wcs_from_footprints, + _validate_wcs_list, ) @@ -177,3 +178,43 @@ def test_wcs_from_footprints(): # expected position onto wcs_1 and wcs_2 assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + + +def test_validate_wcs_list(): + shape = (3, 3) # in pixels + fiducial_world = (10, 0) # in deg + pscale = (0.000028, 0.000028) # in deg/pixel + + dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) + wcs_1 = dm_1.meta.wcs + + # shift fiducial by one pixel in both directions and create a new WCS + fiducial_world = ( + fiducial_world[0] - 0.000028, + fiducial_world[1] - 0.000028, + ) + dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) + wcs_2 = dm_2.meta.wcs + + wcs_list = [wcs_1, wcs_2] + + assert _validate_wcs_list(wcs_list) == True + + +@pytest.mark.parametrize( + "wcs_list, expected_error", + [ + ([], TypeError), + ([1, 2, 3], TypeError), + (["1", "2", "3"], TypeError), + (["1", None, []], TypeError), + ("1", TypeError), + (1, ValueError), + (None, ValueError), + ], +) +def test_validate_wcs_list_invalid(wcs_list, expected_error): + with pytest.raises(Exception) as exec_info: + result = _validate_wcs_list(wcs_list) + + assert type(exec_info.value) == expected_error From b72978b8c474b2c9d5fea9f8895df4bdbe989fe1 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:27:18 -0400 Subject: [PATCH 052/117] Silencing style check warning F403. --- src/stcal/alignment/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/__init__.py b/src/stcal/alignment/__init__.py index 46d3a156..e870d4bd 100644 --- a/src/stcal/alignment/__init__.py +++ b/src/stcal/alignment/__init__.py @@ -1 +1 @@ -from .util import * +from .util import * # noqa: F403 From 8f1828a85a5cfe7b75281ba39783cc9450661ca9 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:28:01 -0400 Subject: [PATCH 053/117] Small style refactoring. --- tests/test_alignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 70138581..5f7ab792 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -198,7 +198,7 @@ def test_validate_wcs_list(): wcs_list = [wcs_1, wcs_2] - assert _validate_wcs_list(wcs_list) == True + assert _validate_wcs_list(wcs_list) @pytest.mark.parametrize( @@ -215,6 +215,6 @@ def test_validate_wcs_list(): ) def test_validate_wcs_list_invalid(wcs_list, expected_error): with pytest.raises(Exception) as exec_info: - result = _validate_wcs_list(wcs_list) + _validate_wcs_list(wcs_list) assert type(exec_info.value) == expected_error From 9c7ee1e2b8146ce910f3b6fe55bf3dc3bfe27990 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:31:59 -0400 Subject: [PATCH 054/117] Set minimum asdf version. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9522b45c..1acb2105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', - 'asdf', + 'asdf >=2.15.0', 'gwcs', ] dynamic = ['version'] From f2d5e21553a6b4301e52da78d0e9f0e9c010d10d Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 15:43:14 -0400 Subject: [PATCH 055/117] Update matplotlib's inventory URL. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d0fe5754..58e3f7d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ def setup(app): "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/devdocs", None), "scipy": ("http://scipy.github.io/devdocs", None), - "matplotlib": ("http://matplotlib.org/", None), + "matplotlib": ("https://matplotlib.org/stable", None), } extensions = [ From 6649fa5fe5945e09fddb047834beb6097de823a0 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 16:04:17 -0400 Subject: [PATCH 056/117] Set lower version for gwcs. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1acb2105..70cd5673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs', + 'gwcs >=0.18.3', ] dynamic = ['version'] From f83f041afdac10fa0ecaeb1adb11d81c3852d4f9 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 16:18:36 -0400 Subject: [PATCH 057/117] Bump astropy version to >= 5.1. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 70cd5673..36eb101c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,12 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.0.4', + 'astropy >=5.1', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs >=0.18.3', + 'gwcs >= 0.18.0', ] dynamic = ['version'] From ae168c0a8f75e5dad6dacb870a03e9e7152c4df0 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 14 Jul 2023 12:27:03 -0400 Subject: [PATCH 058/117] Fix docs style issues. --- docs/Makefile | 2 + docs/conf.py | 19 +- pyproject.toml | 7 +- src/stcal/alignment/util.py | 504 ++++++++++++++++++------------------ 4 files changed, 276 insertions(+), 256 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index bcca5213..1235f237 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,6 +6,7 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build BUILDDIR = _build +APIDIR = api # Internal variables ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . @@ -25,6 +26,7 @@ help: clean: -rm -rf $(BUILDDIR)/* + -rm -rf $(APIDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/conf.py b/docs/conf.py index 58e3f7d3..5ebcee52 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,12 +45,27 @@ def setup(app): "numpy": ("https://numpy.org/devdocs", None), "scipy": ("http://scipy.github.io/devdocs", None), "matplotlib": ("https://matplotlib.org/stable", None), + "gwcs": ("https://gwcs.readthedocs.io/en/latest/", None), + "astropy": ("https://docs.astropy.org/en/stable/", None), + "stdatamodels": ("https://stdatamodels.readthedocs.io/en/latest/", None), } extensions = [ - "sphinx_automodapi.automodapi", + "pytest_doctestplus.sphinx.doctestplus", + "sphinx.ext.autodoc", "sphinx.ext.intersphinx", - "numpydoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx_automodapi.automodapi", + "sphinx_automodapi.automodsumm", + "sphinx_automodapi.autodoc_enhancements", + "sphinx_automodapi.smart_resolver", + "sphinx_asdf", + "sphinx.ext.mathjax", ] autosummary_generate = True diff --git a/pyproject.toml b/pyproject.toml index 36eb101c..b2e1b99c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,12 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.1', + 'astropy >=5.0.4', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', 'asdf >=2.15.0', - 'gwcs >= 0.18.0', + 'gwcs >= 0.18.1', ] dynamic = ['version'] @@ -26,10 +26,11 @@ docs = [ 'numpydoc', 'packaging >=17', 'sphinx', + 'sphinx-asdf', 'sphinx-astropy', 'sphinx-rtd-theme', 'stsci-rtd-theme', - 'tomli; python_version <"3.11"', + 'tomli; python_version <="3.11"', ] test = [ 'psutil', diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index a2df71a4..39697617 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -23,10 +23,10 @@ __all__ = [ - "wcs_from_footprints", "compute_scale", "compute_fiducial", "calc_rotation_matrix", + "wcs_from_footprints", ] @@ -37,114 +37,6 @@ def to_flat_dict(): ... -def compute_scale( - wcs: WCS, - fiducial: Union[tuple, np.ndarray], - disp_axis: int = None, - pscale_ratio: float = None, -) -> float: - """Compute scaling transform. - - Parameters - ---------- - wcs : `~gwcs.wcs.WCS` - Reference WCS object from which to compute a scaling factor. - - fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating - reference points. - - disp_axis : int - Dispersion axis integer. Assumes the same convention as - `wcsinfo.dispersion_direction` - - pscale_ratio : int - Ratio of input to output pixel scale - - Returns - ------- - scale : float - Scaling factor for x and y or cross-dispersion direction. - - """ - spectral = "SPECTRAL" in wcs.output_frame.axes_type - - if spectral and disp_axis is None: - raise ValueError("If input WCS is spectral, a disp_axis must be given") - - crpix = np.array(wcs.invert(*fiducial)) - - delta = np.zeros_like(crpix) - spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] - delta[spatial_idx[0]] = 1 - - crpix_with_offsets = np.vstack( - (crpix, crpix + delta, crpix + np.roll(delta, 1)) - ).T - crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) - - coords = SkyCoord( - ra=crval_with_offsets[spatial_idx[0]], - dec=crval_with_offsets[spatial_idx[1]], - unit="deg", - ) - xscale = np.abs(coords[0].separation(coords[1]).value) - yscale = np.abs(coords[0].separation(coords[2]).value) - - if pscale_ratio is not None: - xscale *= pscale_ratio - yscale *= pscale_ratio - - if spectral: - # Assuming scale doesn't change with wavelength - # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction - return yscale if disp_axis == 1 else xscale - - return np.sqrt(xscale * yscale) - - -def calc_rotation_matrix( - roll_ref: float, v3i_yangle: float, vparity: int = 1 -) -> List[float]: - """Calculate the rotation matrix. - - Parameters - ---------- - roll_ref : float - Telescope roll angle of V3 North over East at the ref. point in radians - - v3i_yangle : float - The angle between ideal Y-axis and V3 in radians. - - vparity : int - The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. - Value should be "1" or "-1". - - Returns - ------- - matrix: [pc1_1, pc1_2, pc2_1, pc2_2] - The rotation matrix - - Notes - ----- - The rotation is - pc1_1 | pc2_1 - - pc1_2 | pc2_2 - """ - if vparity not in (1, -1): - raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - - rel_angle = roll_ref - (vparity * v3i_yangle) - - pc1_1 = vparity * np.cos(rel_angle) - pc1_2 = np.sin(rel_angle) - pc2_1 = vparity * -np.sin(rel_angle) - pc2_2 = np.cos(rel_angle) - - return [pc1_1, pc1_2, pc2_1, pc2_2] - - def _calculate_fiducial_from_spatial_footprint( spatial_footprint: np.ndarray, ) -> np.ndarray: @@ -153,13 +45,13 @@ def _calculate_fiducial_from_spatial_footprint( Parameters ---------- - spatial_footprint : `~numpy.ndarray` + spatial_footprint : numpy.ndarray A 2xN array containing the world coordinates of the WCS footprint's bounding box, where N is the number of bounding box positions. Returns ------- - lon_fiducial, lat_fiducial : `numpy.ndarray`, `numpy.ndarray` + lon_fiducial, lat_fiducial : numpy.ndarray, numpy.ndarray The world coordinates of the fiducial point in the output coordinate frame. """ lon, lat = spatial_footprint @@ -178,99 +70,6 @@ def _calculate_fiducial_from_spatial_footprint( return lon_fiducial, lat_fiducial -def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: - """ - Calculates the world coordinates of the fiducial point of a list of WCS objects. - For a celestial footprint this is the center. For a spectral footprint, it is the - beginning of its range. - - Parameters - ---------- - wcslist : list - A list containing all the WCS objects for which the fiducial is to be - calculated. - - bounding_box : tuple, or list, optional - The bounding box over which the WCS is valid. It can be a either tuple of tuples - or a list of lists of size 2 where each element represents a range of - (low, high) values. The bounding_box is in the order of the axes, axes_order. - For two inputs and axes_order(0, 1) the bounding box can be either - ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. - - Returns - ------- - fiducial : `numpy.ndarray` - A two-elements array containing the world coordinates of the fiducial point - in the combined output coordinate frame. - - Notes - ----- - This function assumes all WCSs have the same output coordinate frame. - """ - - axes_types = wcslist[0].output_frame.axes_type - spatial_axes = np.array(axes_types) == "SPATIAL" - spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack( - [w.footprint(bounding_box=bounding_box).T for w in wcslist] - ) - spatial_footprint = footprints[spatial_axes] - spectral_footprint = footprints[spectral_axes] - - fiducial = np.empty(len(axes_types)) - if spatial_footprint.any(): - fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( - spatial_footprint - ) - if spectral_footprint.any(): - fiducial[spectral_axes] = spectral_footprint.min() - return fiducial - - -def wcsinfo_from_model(input_model: SupportsDataWithWcs): - """ - Creates a dict {wcs_keyword: array_of_values} pairs from a data model. - - Parameters - ---------- - input_model : `~stdatamodels.jwst.datamodels.JwstDataModel` - The input data model. - - Returns - ------- - wcsinfo : dict - A dict containing the WCS FITS keywords and corresponding values. - - """ - defaults = { - "CRPIX": 0, - "CRVAL": 0, - "CDELT": 1.0, - "CTYPE": "", - "CUNIT": u.Unit(""), - } - wcsaxes = input_model.meta.wcsinfo.wcsaxes - wcsinfo = {"WCSAXES": wcsaxes} - for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: - val = [] - for ax in range(1, wcsaxes + 1): - k = (key + "{0}".format(ax)).lower() - v = getattr(input_model.meta.wcsinfo, k, defaults[key]) - val.append(v) - wcsinfo[key] = np.array(val) - - pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) - for i in range(1, wcsaxes + 1): - for j in range(1, wcsaxes + 1): - pc[i - 1, j - 1] = getattr( - input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 - ) - wcsinfo["PC"] = pc - wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame - wcsinfo["has_cd"] = False - return wcsinfo - - def _generate_tranform( refmodel: SupportsDataWithWcs, ref_fiducial: np.array, @@ -285,19 +84,19 @@ def _generate_tranform( Parameters ---------- - refmodel : a valid datamodel + refmodel : The datamodel that should be used as reference for calculating the transform parameters. - pscale_ratio : int, None, optional + pscale_ratio : int, None Ratio of input to output pixel scale. This parameter is only used when - pscale=`None` and, in that case, it is passed on to `compute_scale`. + ``pscale=None`` and, in that case, it is passed on to ``compute_scale``. - pscale : float, None, optional + pscale : float, None The plate scale. If `None`, the plate scale is calculated from the reference datamodel. - rotation : float, None, optional + rotation : float, None Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. The default of `None` specifies that the images will not be rotated, @@ -305,18 +104,18 @@ def _generate_tranform( with the x and y axes of the resampled image corresponding approximately to the detector axes. Ignored when ``transform`` is provided. If `None`, the rotation angle is extracted from the - reference model's `meta.wcsinfo.roll_ref`. + reference model's ``meta.wcsinfo.roll_ref``. - ref_fiducial : np.array + ref_fiducial : numpy.array A two-elements array containing the world coordinates of the fiducial point. - transform : `~astropy.modeling.Model`, optional + transform : ~astropy.modeling.Model A transform between frames. Returns ------- - transform : `~astropy.modeling.Model` - An `~astropy` model containing the transform between frames. + transform : ~astropy.modeling.Model + An :py:mod:`~astropy` model containing the transform between frames. """ if transform is None: sky_axes = refmodel.meta.wcs._get_axes_indices().tolist() @@ -359,21 +158,21 @@ def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference datamodel for which to determine the minimum axis values and bounding box. wcs_list : list The list of WCS objects. - ref_wcs : `~gwcs.wcs.WCS` + ref_wcs : ~gwcs.wcs.WCS The reference WCS object. Returns ------- tuple A tuple containing two elements: - 1 - a `numpy.array` with the minimum value in each axis; + 1 - a :py:class:`numpy.ndarray` with the minimum value in each axis; 2 - a tuple containing the bounding box region in the format ((x0_lower, x0_upper), (x1_lower, x1_upper)). """ @@ -417,11 +216,11 @@ def _calculate_fiducial(wcs_list, bounding_box, crval=None): crval : list, optional A reference world coordinate associated with the reference pixel. If not `None`, then the fiducial coordinates of the spatial axes will be updated with the - values from `crval`. + values from ``crval``. Returns ------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the world coordinate of the fiducial point. """ fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) @@ -441,13 +240,13 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Parameters ---------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements containing the world coordinates of the fiducial point. - wcs : `~gwcs.wcs.WCS` + wcs : ~gwcs.wcs.WCS A WCS object. It will be used to determine the - axis_min_values : `~numpy.ndarray` + axis_min_values : numpy.ndarray A two-elements array containing the minimum pixel value for each axis. crpix : list or tuple @@ -455,15 +254,15 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Returns ------- - `~astropy.modeling.Model` + ~astropy.modeling.Model A model with the offsets to be added to the WCS's transform. Notes ----- - If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. - The reason being that, in this case, the offsets will be calculated using the - WCS object to find the pixel coordinates of the fiducial point and then correct it - by the minimum pixel value for each axis. + If ``crpix=None``, then ``fiducial``, ``wcs``, and ``axis_min_values`` must be + provided, in which case, the offsets will be calculated using the WCS object to + find the pixel coordinates of the fiducial point and then correct it by the minimum + pixel value for each axis. """ if ( crpix is None @@ -490,7 +289,7 @@ def _calculate_new_wcs( Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference model to be used when extracting metadata. shape : list @@ -500,21 +299,21 @@ def _calculate_new_wcs( wcs_list : list A list containing WCS objects. - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the location on the sky in some standard coordinate system. crpix : tuple, optional The coordinates of the reference pixel. - transform : `~astropy.modeling.Model`, optional + transform : ~astropy.modeling.Model An optional tranform to be prepended to the transform constructed by the fiducial point. The number of outputs of this transform must equal the number of axes in the coordinate frame. Returns ------- - `~gwcs.wcs.WCS` + wcs_new : ~gwcs.wcs.WCS The new WCS object that corresponds to the combined WCS objects in `wcs_list`. """ wcs_new = wcs_from_fiducial( @@ -576,7 +375,7 @@ def _validate_wcs_list(wcs_list): elif len(wcs_list): if not all(isinstance(w, WCS) for w in wcs_list): raise TypeError( - "All items in 'wcs_list' are to be instances of gwcs.WCS." + "All items in 'wcs_list' are to be instances of gwcs.wcs.WCS." ) else: raise TypeError("'wcs_list' should not be empty.") @@ -584,6 +383,210 @@ def _validate_wcs_list(wcs_list): return True +def wcsinfo_from_model(input_model: SupportsDataWithWcs): + """ + Creates a dict {wcs_keyword: array_of_values} pairs from a datamodel. + + Parameters + ---------- + input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + The input datamodel. + + Returns + ------- + wcsinfo : dict + A dict containing the WCS FITS keywords and corresponding values. + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : ~gwcs.wcs.WCS + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating + reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as + ``wcsinfo.dispersion_direction`` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: + """ + Calculates the world coordinates of the fiducial point of a list of WCS objects. + For a celestial footprint this is the center. For a spectral footprint, it is the + beginning of its range. + + Parameters + ---------- + wcslist : list + A list containing all the WCS objects for which the fiducial is to be + calculated. + + bounding_box : tuple, list, None + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + Returns + ------- + fiducial : numpy.ndarray + A two-elements array containing the world coordinates of the fiducial point + in the combined output coordinate frame. + + Notes + ----- + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( + spatial_footprint + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def calc_rotation_matrix( + roll_ref: float, v3i_yangle: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yangle : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: list + A list containing the rotation matrix elements in column order. + + Notes + ----- + The rotation matrix is + + .. math:: + PC = \\begin{bmatrix} + pc_{1,1} & pc_{2,1} \\\\ + pc_{1,2} & pc_{2,2} + \\end{bmatrix} + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yangle) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + def wcs_from_footprints( dmodels, refmodel=None, @@ -597,7 +600,7 @@ def wcs_from_footprints( crval=None, ): """ - Create a WCS from a list of input data models. + Create a WCS from a list of input datamodels. A fiducial point in the output coordinate frame is created from the footprints of all WCS objects. For a spatial frame this is the center @@ -613,32 +616,31 @@ def wcs_from_footprints( Parameters ---------- - dmodels : list of valid datamodels - A list of data models. + dmodels : list + A list of valid datamodels. - refmodel : a valid datamodel, optional - This model's WCS is used as a reference. - WCS. The output coordinate frame, the projection and a - scaling and rotation transform is created from it. If not supplied - the first model in the list is used as ``refmodel``. + refmodel : + A valid datamodel whose WCS is used as reference for the creation of the output + coordinate frame, projection, and scaling and rotation transforms. + If not supplied the first model in the list is used as ``refmodel``. - transform : `~astropy.modeling.core.Model`, optional - A transform, passed to :meth:`~gwcs.wcstools.wcs_from_fiducial` - If not supplied Scaling | Rotation is computed from ``refmodel``. + transform : ~astropy.modeling.Model + A transform, passed to :py:func:`gwcs.wcstools.wcs_from_fiducial` + If not supplied `Scaling | Rotation` is computed from ``refmodel``. - bounding_box : tuple, optional + bounding_box : tuple Bounding_box of the new WCS. If not supplied it is computed from the bounding_box of all inputs. - pscale_ratio : float, None, optional + pscale_ratio : float, None Ratio of input to output pixel scale. Ignored when either ``transform`` or ``pscale`` are provided. - pscale : float, None, optional + pscale : float, None Absolute pixel scale in degrees. When provided, overrides ``pscale_ratio``. Ignored when ``transform`` is provided. - rotation : float, None, optional + rotation : float, None Position angle of output image's Y-axis relative to North. A value of 0.0 would orient the final output image to be North up. The default of `None` specifies that the images will not be rotated, @@ -647,24 +649,24 @@ def wcs_from_footprints( approximately to the detector axes. Ignored when ``transform`` is provided. - shape : tuple of int, None, optional + shape : tuple of int, None Shape of the image (data array) using ``numpy.ndarray`` convention (``ny`` first and ``nx`` second). This value will be assigned to ``pixel_shape`` and ``array_shape`` properties of the returned WCS object. - crpix : tuple of float, None, optional + crpix : tuple of float, None Position of the reference pixel in the image array. If ``crpix`` is not specified, it will be set to the center of the bounding box of the returned WCS object. - crval : tuple of float, None, optional + crval : tuple of float, None Right ascension and declination of the reference pixel. Automatically computed if not provided. Returns ------- - wcs_new : `~gwcs.wcs.WCS` + wcs_new : ~gwcs.wcs.WCS The WCS object corresponding to the combined input footprints. """ From dc47316ba385f3849c55aa9729d8e15156f1c83f Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 14 Jul 2023 12:34:41 -0400 Subject: [PATCH 059/117] Style check fix. --- src/stcal/alignment/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 39697617..26333f2e 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -567,7 +567,7 @@ def calc_rotation_matrix( Notes ----- The rotation matrix is - + .. math:: PC = \\begin{bmatrix} pc_{1,1} & pc_{2,1} \\\\ From bc7fdbc89f410e44e54db0af3c66b5ef8acb00bf Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 18 Jul 2023 12:35:25 -0400 Subject: [PATCH 060/117] Fix issue with bounding boxes in tests. --- tests/test_alignment.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 5f7ab792..8888fcf0 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -49,8 +49,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (-0.5, shape[0] - 0.5), - (-0.5, shape[0] - 0.5), + (0, shape[0] - 1), + (0, shape[0] - 1), ) return wcs_obj @@ -158,7 +158,8 @@ def test_wcs_from_footprints(): dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs - # shift fiducial by one pixel in both directions and create a new WCS + # shift fiducial by the size of a pixel projected onto the sky in both directions + # and create a new WCS fiducial_world = ( fiducial_world[0] - 0.000028, fiducial_world[1] - 0.000028, @@ -166,18 +167,12 @@ def test_wcs_from_footprints(): dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs - # check overlapping pixels have approximate the same world coordinate - assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) - assert all(np.isclose(wcs_1(1, 0), wcs_2(2, 1))) - assert all(np.isclose(wcs_1(0, 0), wcs_2(1, 1))) - assert all(np.isclose(wcs_1(1, 1), wcs_2(2, 2))) - wcs = wcs_from_footprints([dm_1, dm_2]) # check that center of calculated WCS matches the # expected position onto wcs_1 and wcs_2 - assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) - assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + assert all(np.isclose(wcs(2, 2), wcs_1(1, 1))) + assert all(np.isclose(wcs(2, 2), wcs_2(2, 2))) def test_validate_wcs_list(): From 6fe6df84e277126f70499dabfef40025c43b555b Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 18 Jul 2023 12:35:25 -0400 Subject: [PATCH 061/117] Fix issue with bounding boxes in tests. --- tests/test_alignment.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 5f7ab792..8888fcf0 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -49,8 +49,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (-0.5, shape[0] - 0.5), - (-0.5, shape[0] - 0.5), + (0, shape[0] - 1), + (0, shape[0] - 1), ) return wcs_obj @@ -158,7 +158,8 @@ def test_wcs_from_footprints(): dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs - # shift fiducial by one pixel in both directions and create a new WCS + # shift fiducial by the size of a pixel projected onto the sky in both directions + # and create a new WCS fiducial_world = ( fiducial_world[0] - 0.000028, fiducial_world[1] - 0.000028, @@ -166,18 +167,12 @@ def test_wcs_from_footprints(): dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs - # check overlapping pixels have approximate the same world coordinate - assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) - assert all(np.isclose(wcs_1(1, 0), wcs_2(2, 1))) - assert all(np.isclose(wcs_1(0, 0), wcs_2(1, 1))) - assert all(np.isclose(wcs_1(1, 1), wcs_2(2, 2))) - wcs = wcs_from_footprints([dm_1, dm_2]) # check that center of calculated WCS matches the # expected position onto wcs_1 and wcs_2 - assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) - assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + assert all(np.isclose(wcs(2, 2), wcs_1(1, 1))) + assert all(np.isclose(wcs(2, 2), wcs_2(2, 2))) def test_validate_wcs_list(): From f03a11cb6e5eff191c2380e974884aea0d531293 Mon Sep 17 00:00:00 2001 From: mairan Date: Wed, 2 Aug 2023 10:37:06 -0400 Subject: [PATCH 062/117] Fix issue with bounding box when creating WCS for tests. (#181) * Add methods necessary for resample. * Small changes to accommodate both JWST and RST datamodels. * Add CHANGES.rst entry. * Add PR number to CHANGES.rst entry. * use protocols * temp * add dependencies * add CI testing to alignment branch * add CI testing to alignment branch * fix test * Add setting of number_extended_events (#178) * Add setting of number_extended_events This was not being set when in single processing mode. * Update CHANGES.rst * Update CHANGES.rst * Update test_jump.py --------- Co-authored-by: Howard Bushouse * Rename variables with FITS keywords names. * Style reformating. * Update docs to include stcal.alignment. * Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. * Updates to address most comments. * Fix misplaced comment. * Code refactoring and additional unit test. * Silencing style check warning F403. * Small style refactoring. * Set minimum asdf version. * Update matplotlib's inventory URL. * Set lower version for gwcs. * Bump astropy version to >= 5.1. * Fix docs style issues. * Style check fix. * Fix issue with bounding boxes in tests. * Fix issue with bounding boxes in tests. --------- Co-authored-by: Nadia Dencheva Co-authored-by: mwregan2 Co-authored-by: Howard Bushouse --- tests/test_alignment.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 5f7ab792..8888fcf0 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -49,8 +49,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (-0.5, shape[0] - 0.5), - (-0.5, shape[0] - 0.5), + (0, shape[0] - 1), + (0, shape[0] - 1), ) return wcs_obj @@ -158,7 +158,8 @@ def test_wcs_from_footprints(): dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs - # shift fiducial by one pixel in both directions and create a new WCS + # shift fiducial by the size of a pixel projected onto the sky in both directions + # and create a new WCS fiducial_world = ( fiducial_world[0] - 0.000028, fiducial_world[1] - 0.000028, @@ -166,18 +167,12 @@ def test_wcs_from_footprints(): dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_2 = dm_2.meta.wcs - # check overlapping pixels have approximate the same world coordinate - assert all(np.isclose(wcs_1(0, 1), wcs_2(1, 2))) - assert all(np.isclose(wcs_1(1, 0), wcs_2(2, 1))) - assert all(np.isclose(wcs_1(0, 0), wcs_2(1, 1))) - assert all(np.isclose(wcs_1(1, 1), wcs_2(2, 2))) - wcs = wcs_from_footprints([dm_1, dm_2]) # check that center of calculated WCS matches the # expected position onto wcs_1 and wcs_2 - assert all(np.isclose(wcs(2, 2), wcs_1(0.5, 0.5))) - assert all(np.isclose(wcs(2, 2), wcs_2(1.5, 1.5))) + assert all(np.isclose(wcs(2, 2), wcs_1(1, 1))) + assert all(np.isclose(wcs(2, 2), wcs_2(2, 2))) def test_validate_wcs_list(): From c09bd55f3c17d6d1b17b14633cda3493adba3f4a Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 10 Aug 2023 13:01:19 -0400 Subject: [PATCH 063/117] Add new methods required by resample_step. --- src/stcal/alignment/util.py | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 26333f2e..d65ab014 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -700,3 +700,64 @@ def wcs_from_footprints( fiducial=fiducial, transform=transform, ) + + +def update_s_region_imaging(model): + """ + Update the ``S_REGION`` keyword using ``WCS.footprint``. + """ + + bbox = model.meta.wcs.bounding_box + + if bbox is None: + bbox = wcs_bbox_from_shape(model.data.shape) + + # footprint is an array of shape (2, 4) as we + # are interested only in the footprint on the sky + footprint = model.meta.wcs.footprint( + bbox, center=True, axis_type="spatial" + ).T + # take only imaging footprint + footprint = footprint[:2, :] + + # Make sure RA values are all positive + negative_ind = footprint[0] < 0 + if negative_ind.any(): + footprint[0][negative_ind] = 360 + footprint[0][negative_ind] + + footprint = footprint.T + update_s_region_keyword(model, footprint) + + +def wcs_bbox_from_shape(shape): + """Create a bounding box from the shape of the data. + + This is appropriate to attach to a wcs object + Parameters + ---------- + shape : tuple + The shape attribute from a `numpy.ndarray` array + + Returns + ------- + bbox : tuple + Bounding box in x, y order. + """ + return (-0.5, shape[-1] - 0.5), (-0.5, shape[-2] - 0.5) + + +def update_s_region_keyword(model, footprint): + """Update the S_REGION keyword.""" + s_region = ( + "POLYGON ICRS " + " {0:.9f} {1:.9f}" + " {2:.9f} {3:.9f}" + " {4:.9f} {5:.9f}" + " {6:.9f} {7:.9f}".format(*footprint.flatten()) + ) + if "nan" in s_region: + # do not update s_region if there are NaNs. + log.info("There are NaNs in s_region, S_REGION not updated.") + else: + model.meta.wcsinfo.s_region = s_region + log.info(f"Update S_REGION to {model.meta.wcsinfo.s_region}") From 176845fb57df9eb78cbaf361402259903b890bcf Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 17 Aug 2023 10:46:46 -0400 Subject: [PATCH 064/117] Small refactoring and clean ups. --- src/stcal/alignment/util.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index d65ab014..b8a556ad 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -290,7 +290,7 @@ def _calculate_new_wcs( Parameters ---------- ref_model : - The reference model to be used when extracting metadata. + The reference datamodel to be used when extracting metadata. shape : list The shape of the new WCS's pixel grid. If `None`, then the output bounding box @@ -389,7 +389,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): Parameters ---------- - input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + input_model : The input datamodel. Returns @@ -702,9 +702,17 @@ def wcs_from_footprints( ) -def update_s_region_imaging(model): +def update_s_region_imaging(model, center=True): """ Update the ``S_REGION`` keyword using ``WCS.footprint``. + + Parameters + ---------- + model : + The input datamodel. + center : bool, optional + Whether or not to use the center of the pixel as reference for the + coordinates, by default True """ bbox = model.meta.wcs.bounding_box @@ -714,8 +722,13 @@ def update_s_region_imaging(model): # footprint is an array of shape (2, 4) as we # are interested only in the footprint on the sky + ### TODO: we shouldn't use center=True in the call below because we want to + ### calculate the coordinates of the footprint based on the *bounding box*, + ### which means we are interested in each pixel's vertice, not its center. + ### By using center=True, a difference of 0.5 pixel should be accounted for + ### when comparing the world coordinates of the bounding box and the footprint. footprint = model.meta.wcs.footprint( - bbox, center=True, axis_type="spatial" + bbox, center=center, axis_type="spatial" ).T # take only imaging footprint footprint = footprint[:2, :] @@ -747,7 +760,20 @@ def wcs_bbox_from_shape(shape): def update_s_region_keyword(model, footprint): - """Update the S_REGION keyword.""" + """Update the S_REGION keyword. + + Parameters + ---------- + model : + The input model + footprint : numpy.array + A 4x2 numpy array containing the coordinates of the vertices of the footprint. + + Returns + ------- + s_region : str + String containing the S_REGION object. + """ s_region = ( "POLYGON ICRS " " {0:.9f} {1:.9f}" From bc706e90662270e4aa8094fa9297845dacd763a9 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 17 Aug 2023 10:47:50 -0400 Subject: [PATCH 065/117] RCAL-632: add unit tests for new methods. --- tests/test_alignment.py | 113 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 8888fcf0..48cee2dc 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -14,6 +14,9 @@ compute_scale, wcs_from_footprints, _validate_wcs_list, + update_s_region_keyword, + wcs_bbox_from_shape, + update_s_region_imaging, ) @@ -23,7 +26,7 @@ def _create_wcs_object_without_distortion( shape, ): # subtract 1 to account for pixel indexing starting at 0 - shift = models.Shift(-(shape[0] - 1)) & models.Shift(-(shape[1] - 1)) + shift = models.Shift() & models.Shift() scale = models.Scale(pscale[0]) & models.Scale(pscale[1]) @@ -49,8 +52,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (0, shape[0] - 1), - (0, shape[0] - 1), + (-0.5, shape[-1] - 0.5), + (-0.5, shape[-2] - 0.5), ) return wcs_obj @@ -84,6 +87,7 @@ def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle): self.roll_ref = roll_ref self.vparity = -1 self.wcsaxes = 2 + self.s_region = "" class Coordinates: @@ -151,6 +155,14 @@ def test_compute_scale(pscales): def test_wcs_from_footprints(): + """ + Test that the WCS created from wcs_from_footprints has correct vertice coordinates. + + N.B.: this test will create two 3x3 arrays shifted by 0.000028 deg in + both directions, which means that the combined WCS generated by wcs_from_footprints + should be a 4x4 array with its fiducial point coordinates equal to the + first element of its footprint. + """ shape = (3, 3) # in pixels fiducial_world = (10, 0) # in deg pscale = (0.000028, 0.000028) # in deg/pixel @@ -169,10 +181,15 @@ def test_wcs_from_footprints(): wcs = wcs_from_footprints([dm_1, dm_2]) - # check that center of calculated WCS matches the - # expected position onto wcs_1 and wcs_2 - assert all(np.isclose(wcs(2, 2), wcs_1(1, 1))) - assert all(np.isclose(wcs(2, 2), wcs_2(2, 2))) + # check that all elements of footprint match the *vertices* of the new combined WCS + assert all(np.isclose(wcs.footprint()[0], wcs(0, 0))) + assert all(np.isclose(wcs.footprint()[1], wcs(0, 4))) + assert all(np.isclose(wcs.footprint()[2], wcs(4, 4))) + assert all(np.isclose(wcs.footprint()[3], wcs(4, 0))) + + # check that fiducials match their expected coords in the new combined WCS + assert all(np.isclose(wcs_1(0, 0), wcs(2.5, 1.5))) + assert all(np.isclose(wcs_2(0, 0), wcs(3.5, 0.5))) def test_validate_wcs_list(): @@ -213,3 +230,85 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): _validate_wcs_list(wcs_list) assert type(exec_info.value) == expected_error + + +@pytest.mark.parametrize( + "model, footprint, expected_s_region, expected_log_info", + [ + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]), + "POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", + "Update S_REGION to POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", + ), + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + np.array([[1.0, 2.0], [3.0, np.nan], [5.0, 6.0], [7.0, 8.0]]), + "", + "There are NaNs in s_region, S_REGION not updated.", + ), + ], +) +def test_update_s_region_keyword( + model, footprint, expected_s_region, expected_log_info, caplog +): + """ + Test that S_REGION keyword is being properly populated with the coordinate values. + """ + update_s_region_keyword(model, footprint) + assert model.meta.wcsinfo.s_region == expected_s_region + assert expected_log_info in caplog.text + + +@pytest.mark.parametrize( + "shape, expected_bbox", + [ + ((100, 200), ((-0.5, 199.5), (-0.5, 99.5))), + ((1, 1), ((-0.5, 0.5), (-0.5, 0.5))), + ((0, 0), ((-0.5, -0.5), (-0.5, -0.5))), + ], +) +def test_wcs_bbox_from_shape(shape, expected_bbox): + """ + Test that the bounding box generated by wcs_bbox_from_shape is correct. + """ + bbox = wcs_bbox_from_shape(shape) + assert bbox == expected_bbox + + +@pytest.mark.parametrize( + "model, bounding_box, data", + [ + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + ((-0.5, 2.5), (-0.5, 2.5)), + None, + ), + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + None, + np.zeros((3, 3)), + ), + ], +) +def test_update_s_region_imaging(model, bounding_box, data): + """ + Test that S_REGION keyword is being properly updated with the coordinates + corresponding to the footprint (same as WCS(bounding box)). + """ + model.data = data + model.meta.wcs.bounding_box = bounding_box + expected_s_region_coords = [ + *model.meta.wcs(-0.5, -0.5), + *model.meta.wcs(-0.5, 2.5), + *model.meta.wcs(2.5, 2.5), + *model.meta.wcs(2.5, -0.5), + ] + update_s_region_imaging(model, center=False) + updated_s_region_coords = [ + float(x) for x in model.meta.wcsinfo.s_region.split(" ")[3:] + ] + assert all( + np.isclose(x, y) + for x, y in zip(updated_s_region_coords, expected_s_region_coords) + ) From 05227798747d6f2f964fdf3db571599f827cd115 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 17 Aug 2023 16:01:14 -0400 Subject: [PATCH 066/117] Fix style check errors. --- tests/test_alignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 48cee2dc..2198990c 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -238,8 +238,8 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): ( _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]), - "POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", - "Update S_REGION to POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", + "POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", # noqa: E501 + "Update S_REGION to POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", # noqa: E501 ), ( _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), From 3baee3ba572e55ef77c33b00ae45625a775468b1 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 17 Aug 2023 16:17:20 -0400 Subject: [PATCH 067/117] Set sphinx version for compatibility with sphinx-rtd-theme. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b2e1b99c..2da58d8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dynamic = ['version'] docs = [ 'numpydoc', 'packaging >=17', - 'sphinx', + 'sphinx<7.0.0', 'sphinx-asdf', 'sphinx-astropy', 'sphinx-rtd-theme', From 7d03957d76d536abf0eb5a1643ace4f00b7761f9 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:11:12 -0400 Subject: [PATCH 068/117] Added working module. --- src/stcal/__init__.py | 4 +- src/stcal/alignment/reproject.py | 82 +++++++++++++++++++++ src/stcal/alignment/resample_utils.py | 0 src/stcal/alignment/tests/test_reproject.py | 14 ++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/stcal/alignment/reproject.py delete mode 100644 src/stcal/alignment/resample_utils.py create mode 100644 src/stcal/alignment/tests/test_reproject.py diff --git a/src/stcal/__init__.py b/src/stcal/__init__.py index b869c093..6ae09ecc 100644 --- a/src/stcal/__init__.py +++ b/src/stcal/__init__.py @@ -1,4 +1,4 @@ -from ._version import version as __version__ +# from ._version import version as __version__ -__all__ = ['__version__'] +# __all__ = ['__version__'] diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py new file mode 100644 index 00000000..97ec9edf --- /dev/null +++ b/src/stcal/alignment/reproject.py @@ -0,0 +1,82 @@ +from astropy import wcs as fitswcs +from astropy.modeling import Model +import gwcs +import numpy as np + + +def reproject_coords(wcs1, wcs2): + """ + Given two WCSs or transforms return a function which takes pixel + coordinates in the first WCS or transform and computes them in the second + one. It performs the forward transformation of ``wcs1`` followed by the + inverse of ``wcs2``. + + Parameters + ---------- + wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + WCS objects. + + Returns + ------- + _reproject : func + Function to compute the transformations. It takes x, y + positions in ``wcs1`` and returns x, y positions in ``wcs2``. + """ + + def _get_forward_transform_func(wcs1): + """Get the forward transform function from the input WCS. If the wcs is a + fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), + y (str, ndarray), and origin (int). The origin should be between 0, and 1, representing an origin pixel at t + https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file + ) + """ + if isinstance(wcs1, fitswcs.WCS): + forward_transform = wcs1.all_pix2world + elif isinstance(wcs1, gwcs.WCS): + forward_transform = wcs1.forward_transform + elif issubclass(wcs1, Model): + forward_transform = wcs1 + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return forward_transform + + def _get_backward_transform_func(wcs2): + if isinstance(wcs2, fitswcs.WCS): + backward_transform = wcs2.all_world2pix + elif isinstance(wcs2, gwcs.WCS): + backward_transform = wcs2.backward_transform + elif issubclass(wcs2, Model): + backward_transform = wcs2.inverse + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return backward_transform + + def _reproject(x: np.ndarray, y: np.ndarray) -> tuple: + """ + Reprojects the input coordinates from one WCS to another. + + Parameters: + ----------- + x : str or np.ndarray + Array of x-coordinates to be reprojected. + y : str or np.ndarray + Array of y-coordinates to be reprojected. + + Returns: + -------- + tuple + Tuple of reprojected x and y coordinates. + """ + # example inputs to resulting function (12, 13, 0) # third number is origin + sky = _get_forward_transform_func(wcs1)(x, y, 0) + sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) + new_sky = tuple(sky_back[:, :1].flatten()) + return tuple(new_sky) + + return _reproject diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py new file mode 100644 index 00000000..6380f199 --- /dev/null +++ b/src/stcal/alignment/tests/test_reproject.py @@ -0,0 +1,14 @@ +import numpy as np +from astropy.io import fits +from stcal.alignment import reproject + + + +def test__reproject(): + x_inp, y_inp = 1000, 2000 + # Create a test image with a single pixel + fake_wcs1 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.1, 'CDELT2': 0.1}) + fake_wcs2 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.05, 'CDELT2': 0.05}) + + # Call the function + reproject.reproject_coords(fake_wcs1, fake_wcs2) \ No newline at end of file From 88e90f5a905be6bae4711bcba5260c51bb4e5ced Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:06:47 -0400 Subject: [PATCH 069/117] Added functional unit testing --- src/stcal/alignment/reproject.py | 22 ++++-- src/stcal/alignment/testing_reproject.ipynb | 79 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/stcal/alignment/testing_reproject.ipynb diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index 97ec9edf..3bb9a0af 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -1,14 +1,15 @@ -from astropy import wcs as fitswcs -from astropy.modeling import Model import gwcs import numpy as np +from astropy import wcs as fitswcs +from astropy.modeling import Model +from typing import Union def reproject_coords(wcs1, wcs2): """ Given two WCSs or transforms return a function which takes pixel - coordinates in the first WCS or transform and computes them in the second - one. It performs the forward transformation of ``wcs1`` followed by the + coordinates in the first WCS or transform and computes them in pixel coordinates + in the second one. It performs the forward transformation of ``wcs1`` followed by the inverse of ``wcs2``. Parameters @@ -24,8 +25,8 @@ def reproject_coords(wcs1, wcs2): """ def _get_forward_transform_func(wcs1): - """Get the forward transform function from the input WCS. If the wcs is a - fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), + """Get the forward transform function from the input WCS. If the wcs is a + fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), y (str, ndarray), and origin (int). The origin should be between 0, and 1, representing an origin pixel at t https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) @@ -57,7 +58,7 @@ def _get_backward_transform_func(wcs2): ) return backward_transform - def _reproject(x: np.ndarray, y: np.ndarray) -> tuple: + def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: """ Reprojects the input coordinates from one WCS to another. @@ -74,6 +75,13 @@ def _reproject(x: np.ndarray, y: np.ndarray) -> tuple: Tuple of reprojected x and y coordinates. """ # example inputs to resulting function (12, 13, 0) # third number is origin + if not isinstance(x, list): + x = [x] + if not isinstance(y, list): + y = [y] + print(x) + if len(x) != len(y): + raise ValueError("x and y must be the same length") sky = _get_forward_transform_func(wcs1)(x, y, 0) sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) new_sky = tuple(sky_back[:, :1].flatten()) diff --git a/src/stcal/alignment/testing_reproject.ipynb b/src/stcal/alignment/testing_reproject.ipynb new file mode 100644 index 00000000..b63f4c71 --- /dev/null +++ b/src/stcal/alignment/testing_reproject.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from reproject import reproject_coords\n", + "from stwcs.wcsutil import HSTWCS\n", + "from astropy.wcs import WCS\n", + "from astropy.io import fits\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(2999.9999999998668, 2999.9999999999286)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hdu1=fits.open('hst_12546_22_acs_wfc_f606w_jbt122x1_drc.fits')\n", + "hdu2=fits.open('hst_12546_22_acs_wfc_f814w_jbt122_drc.fits')\n", + "wcs1=WCS(hdu1[1].header)\n", + "wcs2=WCS(hdu2[1].header)\n", + "# wcs2 = HSTWCS('jcdm74guq_drc.fits', ext=1)\n", + "f = reproject_coords(wcs1, wcs2)\n", + "f([3000], [3000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 474d9170fde42aa5cd47123f8966c7f71f4c3ebd Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:41:34 -0400 Subject: [PATCH 070/117] Added reproject. --- src/stcal/alignment/reproject.py | 2 +- src/stcal/alignment/tests/test_reproject.py | 59 ++++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index 3bb9a0af..068da5cd 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -27,7 +27,7 @@ def reproject_coords(wcs1, wcs2): def _get_forward_transform_func(wcs1): """Get the forward transform function from the input WCS. If the wcs is a fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), - y (str, ndarray), and origin (int). The origin should be between 0, and 1, representing an origin pixel at t + y (str, ndarray), and origin (int). The origin should be between 0, and 1 https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) """ diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py index 6380f199..72cd8e8a 100644 --- a/src/stcal/alignment/tests/test_reproject.py +++ b/src/stcal/alignment/tests/test_reproject.py @@ -1,14 +1,59 @@ +import pytest import numpy as np from astropy.io import fits +from astropy.wcs import WCS from stcal.alignment import reproject +def get_fake_wcs(): + fake_wcs1 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.1, + "CDELT2": 0.1, + } + ) + ) + fake_wcs2 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.05, + "CDELT2": 0.05, + } + ) + ) + return fake_wcs1, fake_wcs2 -def test__reproject(): - x_inp, y_inp = 1000, 2000 - # Create a test image with a single pixel - fake_wcs1 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.1, 'CDELT2': 0.1}) - fake_wcs2 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.05, 'CDELT2': 0.05}) - # Call the function - reproject.reproject_coords(fake_wcs1, fake_wcs2) \ No newline at end of file +@pytest.mark.parametrize( + "x_inp, y_inp, x_expected, y_expected", + [ + (1000, 2000, 2000, 4000), # string input test + ([1000], [2000], 2000, 4000), # array input test + pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test + ], +) +def test__reproject(x_inp, y_inp, x_expected, y_expected): + wcs1, wcs2 = get_fake_wcs() + f = reproject.reproject_coords(wcs1, wcs2) + x_out, y_out = f(x_inp, y_inp) + assert np.allclose(x_out, x_expected, rtol=1e-05) + assert np.allclose(y_out, y_expected, rtol=1e-05) From 142e27671c2137c058574b29d1765a223e212d15 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:42:53 -0400 Subject: [PATCH 071/117] removed print statement --- src/stcal/alignment/reproject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index 068da5cd..daf9a786 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -79,7 +79,6 @@ def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: x = [x] if not isinstance(y, list): y = [y] - print(x) if len(x) != len(y): raise ValueError("x and y must be the same length") sky = _get_forward_transform_func(wcs1)(x, y, 0) From 665f1b37e030c0bf60096f254bcf2d1725376ede Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 11:45:42 -0400 Subject: [PATCH 072/117] Fix style issues. --- src/stcal/alignment/reproject.py | 2 +- src/stcal/alignment/tests/test_reproject.py | 37 +++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index 3bb9a0af..0d0b5fd0 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -30,7 +30,7 @@ def _get_forward_transform_func(wcs1): y (str, ndarray), and origin (int). The origin should be between 0, and 1, representing an origin pixel at t https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) - """ + """ # noqa : E501 if isinstance(wcs1, fitswcs.WCS): forward_transform = wcs1.all_pix2world elif isinstance(wcs1, gwcs.WCS): diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py index 6380f199..0747cb19 100644 --- a/src/stcal/alignment/tests/test_reproject.py +++ b/src/stcal/alignment/tests/test_reproject.py @@ -1,14 +1,39 @@ -import numpy as np from astropy.io import fits from stcal.alignment import reproject - def test__reproject(): - x_inp, y_inp = 1000, 2000 # Create a test image with a single pixel - fake_wcs1 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.1, 'CDELT2': 0.1}) - fake_wcs2 = fits.Header({'NAXIS': 2, 'NAXIS1': 1, 'NAXIS2': 1, 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', 'CRVAL1': 0, 'CRVAL2': 0, 'CRPIX1': 1, 'CRPIX2': 1, 'CDELT1': -0.05, 'CDELT2': 0.05}) + fake_wcs1 = fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.1, + "CDELT2": 0.1, + } + ) + fake_wcs2 = fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.05, + "CDELT2": 0.05, + } + ) # Call the function - reproject.reproject_coords(fake_wcs1, fake_wcs2) \ No newline at end of file + reproject.reproject_coords(fake_wcs1, fake_wcs2) From a89196b569d69916aa72c9b21635172327b7861c Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 12:00:27 -0400 Subject: [PATCH 073/117] Revert reproject commit. --- src/stcal/__init__.py | 4 +- src/stcal/alignment/reproject.py | 90 --------------------- src/stcal/alignment/resample_utils.py | 0 src/stcal/alignment/tests/test_reproject.py | 39 --------- 4 files changed, 2 insertions(+), 131 deletions(-) delete mode 100644 src/stcal/alignment/reproject.py create mode 100644 src/stcal/alignment/resample_utils.py delete mode 100644 src/stcal/alignment/tests/test_reproject.py diff --git a/src/stcal/__init__.py b/src/stcal/__init__.py index 6ae09ecc..b869c093 100644 --- a/src/stcal/__init__.py +++ b/src/stcal/__init__.py @@ -1,4 +1,4 @@ -# from ._version import version as __version__ +from ._version import version as __version__ -# __all__ = ['__version__'] +__all__ = ['__version__'] diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py deleted file mode 100644 index 0d0b5fd0..00000000 --- a/src/stcal/alignment/reproject.py +++ /dev/null @@ -1,90 +0,0 @@ -import gwcs -import numpy as np -from astropy import wcs as fitswcs -from astropy.modeling import Model -from typing import Union - - -def reproject_coords(wcs1, wcs2): - """ - Given two WCSs or transforms return a function which takes pixel - coordinates in the first WCS or transform and computes them in pixel coordinates - in the second one. It performs the forward transformation of ``wcs1`` followed by the - inverse of ``wcs2``. - - Parameters - ---------- - wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` - WCS objects. - - Returns - ------- - _reproject : func - Function to compute the transformations. It takes x, y - positions in ``wcs1`` and returns x, y positions in ``wcs2``. - """ - - def _get_forward_transform_func(wcs1): - """Get the forward transform function from the input WCS. If the wcs is a - fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), - y (str, ndarray), and origin (int). The origin should be between 0, and 1, representing an origin pixel at t - https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file - ) - """ # noqa : E501 - if isinstance(wcs1, fitswcs.WCS): - forward_transform = wcs1.all_pix2world - elif isinstance(wcs1, gwcs.WCS): - forward_transform = wcs1.forward_transform - elif issubclass(wcs1, Model): - forward_transform = wcs1 - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return forward_transform - - def _get_backward_transform_func(wcs2): - if isinstance(wcs2, fitswcs.WCS): - backward_transform = wcs2.all_world2pix - elif isinstance(wcs2, gwcs.WCS): - backward_transform = wcs2.backward_transform - elif issubclass(wcs2, Model): - backward_transform = wcs2.inverse - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return backward_transform - - def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: - """ - Reprojects the input coordinates from one WCS to another. - - Parameters: - ----------- - x : str or np.ndarray - Array of x-coordinates to be reprojected. - y : str or np.ndarray - Array of y-coordinates to be reprojected. - - Returns: - -------- - tuple - Tuple of reprojected x and y coordinates. - """ - # example inputs to resulting function (12, 13, 0) # third number is origin - if not isinstance(x, list): - x = [x] - if not isinstance(y, list): - y = [y] - print(x) - if len(x) != len(y): - raise ValueError("x and y must be the same length") - sky = _get_forward_transform_func(wcs1)(x, y, 0) - sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) - new_sky = tuple(sky_back[:, :1].flatten()) - return tuple(new_sky) - - return _reproject diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py deleted file mode 100644 index 0747cb19..00000000 --- a/src/stcal/alignment/tests/test_reproject.py +++ /dev/null @@ -1,39 +0,0 @@ -from astropy.io import fits -from stcal.alignment import reproject - - -def test__reproject(): - # Create a test image with a single pixel - fake_wcs1 = fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.1, - "CDELT2": 0.1, - } - ) - fake_wcs2 = fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.05, - "CDELT2": 0.05, - } - ) - - # Call the function - reproject.reproject_coords(fake_wcs1, fake_wcs2) From 77bbf756b6491ea4d7ed52be2dee73a7b1a70726 Mon Sep 17 00:00:00 2001 From: mairan Date: Tue, 22 Aug 2023 15:40:31 -0400 Subject: [PATCH 074/117] RCAL-632: add unit tests for additional methods. (#190) * Add methods necessary for resample. * Small changes to accommodate both JWST and RST datamodels. * Add CHANGES.rst entry. * Add PR number to CHANGES.rst entry. * use protocols * temp * add dependencies * add CI testing to alignment branch * add CI testing to alignment branch * fix test * Add setting of number_extended_events (#178) * Add setting of number_extended_events This was not being set when in single processing mode. * Update CHANGES.rst * Update CHANGES.rst * Update test_jump.py --------- Co-authored-by: Howard Bushouse * Rename variables with FITS keywords names. * Style reformating. * Update docs to include stcal.alignment. * Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. * Updates to address most comments. * Fix misplaced comment. * Code refactoring and additional unit test. * Silencing style check warning F403. * Small style refactoring. * Set minimum asdf version. * Update matplotlib's inventory URL. * Set lower version for gwcs. * Bump astropy version to >= 5.1. * Fix docs style issues. * Style check fix. * Fix issue with bounding boxes in tests. * Fix issue with bounding boxes in tests. * Add new methods required by resample_step. * Small refactoring and clean ups. * RCAL-632: add unit tests for new methods. * Fix style check errors. * Set sphinx version for compatibility with sphinx-rtd-theme. * Fix style issues. * Revert reproject commit. --------- Co-authored-by: Nadia Dencheva Co-authored-by: mwregan2 Co-authored-by: Howard Bushouse --- pyproject.toml | 2 +- src/stcal/__init__.py | 4 +- src/stcal/alignment/resample_utils.py | 0 src/stcal/alignment/util.py | 91 ++++++++++++++++++++- tests/test_alignment.py | 113 ++++++++++++++++++++++++-- 5 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 src/stcal/alignment/resample_utils.py diff --git a/pyproject.toml b/pyproject.toml index b2e1b99c..2da58d8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dynamic = ['version'] docs = [ 'numpydoc', 'packaging >=17', - 'sphinx', + 'sphinx<7.0.0', 'sphinx-asdf', 'sphinx-astropy', 'sphinx-rtd-theme', diff --git a/src/stcal/__init__.py b/src/stcal/__init__.py index 6ae09ecc..b869c093 100644 --- a/src/stcal/__init__.py +++ b/src/stcal/__init__.py @@ -1,4 +1,4 @@ -# from ._version import version as __version__ +from ._version import version as __version__ -# __all__ = ['__version__'] +__all__ = ['__version__'] diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 26333f2e..b8a556ad 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -290,7 +290,7 @@ def _calculate_new_wcs( Parameters ---------- ref_model : - The reference model to be used when extracting metadata. + The reference datamodel to be used when extracting metadata. shape : list The shape of the new WCS's pixel grid. If `None`, then the output bounding box @@ -389,7 +389,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): Parameters ---------- - input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + input_model : The input datamodel. Returns @@ -700,3 +700,90 @@ def wcs_from_footprints( fiducial=fiducial, transform=transform, ) + + +def update_s_region_imaging(model, center=True): + """ + Update the ``S_REGION`` keyword using ``WCS.footprint``. + + Parameters + ---------- + model : + The input datamodel. + center : bool, optional + Whether or not to use the center of the pixel as reference for the + coordinates, by default True + """ + + bbox = model.meta.wcs.bounding_box + + if bbox is None: + bbox = wcs_bbox_from_shape(model.data.shape) + + # footprint is an array of shape (2, 4) as we + # are interested only in the footprint on the sky + ### TODO: we shouldn't use center=True in the call below because we want to + ### calculate the coordinates of the footprint based on the *bounding box*, + ### which means we are interested in each pixel's vertice, not its center. + ### By using center=True, a difference of 0.5 pixel should be accounted for + ### when comparing the world coordinates of the bounding box and the footprint. + footprint = model.meta.wcs.footprint( + bbox, center=center, axis_type="spatial" + ).T + # take only imaging footprint + footprint = footprint[:2, :] + + # Make sure RA values are all positive + negative_ind = footprint[0] < 0 + if negative_ind.any(): + footprint[0][negative_ind] = 360 + footprint[0][negative_ind] + + footprint = footprint.T + update_s_region_keyword(model, footprint) + + +def wcs_bbox_from_shape(shape): + """Create a bounding box from the shape of the data. + + This is appropriate to attach to a wcs object + Parameters + ---------- + shape : tuple + The shape attribute from a `numpy.ndarray` array + + Returns + ------- + bbox : tuple + Bounding box in x, y order. + """ + return (-0.5, shape[-1] - 0.5), (-0.5, shape[-2] - 0.5) + + +def update_s_region_keyword(model, footprint): + """Update the S_REGION keyword. + + Parameters + ---------- + model : + The input model + footprint : numpy.array + A 4x2 numpy array containing the coordinates of the vertices of the footprint. + + Returns + ------- + s_region : str + String containing the S_REGION object. + """ + s_region = ( + "POLYGON ICRS " + " {0:.9f} {1:.9f}" + " {2:.9f} {3:.9f}" + " {4:.9f} {5:.9f}" + " {6:.9f} {7:.9f}".format(*footprint.flatten()) + ) + if "nan" in s_region: + # do not update s_region if there are NaNs. + log.info("There are NaNs in s_region, S_REGION not updated.") + else: + model.meta.wcsinfo.s_region = s_region + log.info(f"Update S_REGION to {model.meta.wcsinfo.s_region}") diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 8888fcf0..2198990c 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -14,6 +14,9 @@ compute_scale, wcs_from_footprints, _validate_wcs_list, + update_s_region_keyword, + wcs_bbox_from_shape, + update_s_region_imaging, ) @@ -23,7 +26,7 @@ def _create_wcs_object_without_distortion( shape, ): # subtract 1 to account for pixel indexing starting at 0 - shift = models.Shift(-(shape[0] - 1)) & models.Shift(-(shape[1] - 1)) + shift = models.Shift() & models.Shift() scale = models.Scale(pscale[0]) & models.Scale(pscale[1]) @@ -49,8 +52,8 @@ def _create_wcs_object_without_distortion( wcs_obj = WCS(pipeline) wcs_obj.bounding_box = ( - (0, shape[0] - 1), - (0, shape[0] - 1), + (-0.5, shape[-1] - 0.5), + (-0.5, shape[-2] - 0.5), ) return wcs_obj @@ -84,6 +87,7 @@ def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle): self.roll_ref = roll_ref self.vparity = -1 self.wcsaxes = 2 + self.s_region = "" class Coordinates: @@ -151,6 +155,14 @@ def test_compute_scale(pscales): def test_wcs_from_footprints(): + """ + Test that the WCS created from wcs_from_footprints has correct vertice coordinates. + + N.B.: this test will create two 3x3 arrays shifted by 0.000028 deg in + both directions, which means that the combined WCS generated by wcs_from_footprints + should be a 4x4 array with its fiducial point coordinates equal to the + first element of its footprint. + """ shape = (3, 3) # in pixels fiducial_world = (10, 0) # in deg pscale = (0.000028, 0.000028) # in deg/pixel @@ -169,10 +181,15 @@ def test_wcs_from_footprints(): wcs = wcs_from_footprints([dm_1, dm_2]) - # check that center of calculated WCS matches the - # expected position onto wcs_1 and wcs_2 - assert all(np.isclose(wcs(2, 2), wcs_1(1, 1))) - assert all(np.isclose(wcs(2, 2), wcs_2(2, 2))) + # check that all elements of footprint match the *vertices* of the new combined WCS + assert all(np.isclose(wcs.footprint()[0], wcs(0, 0))) + assert all(np.isclose(wcs.footprint()[1], wcs(0, 4))) + assert all(np.isclose(wcs.footprint()[2], wcs(4, 4))) + assert all(np.isclose(wcs.footprint()[3], wcs(4, 0))) + + # check that fiducials match their expected coords in the new combined WCS + assert all(np.isclose(wcs_1(0, 0), wcs(2.5, 1.5))) + assert all(np.isclose(wcs_2(0, 0), wcs(3.5, 0.5))) def test_validate_wcs_list(): @@ -213,3 +230,85 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): _validate_wcs_list(wcs_list) assert type(exec_info.value) == expected_error + + +@pytest.mark.parametrize( + "model, footprint, expected_s_region, expected_log_info", + [ + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]), + "POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", # noqa: E501 + "Update S_REGION to POLYGON ICRS 1.000000000 2.000000000 3.000000000 4.000000000 5.000000000 6.000000000 7.000000000 8.000000000", # noqa: E501 + ), + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + np.array([[1.0, 2.0], [3.0, np.nan], [5.0, 6.0], [7.0, 8.0]]), + "", + "There are NaNs in s_region, S_REGION not updated.", + ), + ], +) +def test_update_s_region_keyword( + model, footprint, expected_s_region, expected_log_info, caplog +): + """ + Test that S_REGION keyword is being properly populated with the coordinate values. + """ + update_s_region_keyword(model, footprint) + assert model.meta.wcsinfo.s_region == expected_s_region + assert expected_log_info in caplog.text + + +@pytest.mark.parametrize( + "shape, expected_bbox", + [ + ((100, 200), ((-0.5, 199.5), (-0.5, 99.5))), + ((1, 1), ((-0.5, 0.5), (-0.5, 0.5))), + ((0, 0), ((-0.5, -0.5), (-0.5, -0.5))), + ], +) +def test_wcs_bbox_from_shape(shape, expected_bbox): + """ + Test that the bounding box generated by wcs_bbox_from_shape is correct. + """ + bbox = wcs_bbox_from_shape(shape) + assert bbox == expected_bbox + + +@pytest.mark.parametrize( + "model, bounding_box, data", + [ + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + ((-0.5, 2.5), (-0.5, 2.5)), + None, + ), + ( + _create_wcs_and_datamodel((10, 0), (3, 3), (0.000028, 0.000028)), + None, + np.zeros((3, 3)), + ), + ], +) +def test_update_s_region_imaging(model, bounding_box, data): + """ + Test that S_REGION keyword is being properly updated with the coordinates + corresponding to the footprint (same as WCS(bounding box)). + """ + model.data = data + model.meta.wcs.bounding_box = bounding_box + expected_s_region_coords = [ + *model.meta.wcs(-0.5, -0.5), + *model.meta.wcs(-0.5, 2.5), + *model.meta.wcs(2.5, 2.5), + *model.meta.wcs(2.5, -0.5), + ] + update_s_region_imaging(model, center=False) + updated_s_region_coords = [ + float(x) for x in model.meta.wcsinfo.s_region.split(" ")[3:] + ] + assert all( + np.isclose(x, y) + for x, y in zip(updated_s_region_coords, expected_s_region_coords) + ) From 34ea51aa8ff5bd8249a79128b2b291484cb0769f Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 7 Jul 2023 12:01:21 -0400 Subject: [PATCH 075/117] Small changes to accommodate both JWST and RST datamodels. --- tests/test_alignment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 2198990c..9cff3aa2 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -8,7 +8,6 @@ from gwcs import coordinate_frames as cf import pytest - from stcal.alignment.util import ( compute_fiducial, compute_scale, From 1edc1c8e61d090604fe5a8e8dff3aee0c2e94365 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sun, 9 Jul 2023 20:08:15 -0400 Subject: [PATCH 076/117] fix test --- tests/test_alignment.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 9cff3aa2..7d250f65 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -165,7 +165,6 @@ def test_wcs_from_footprints(): shape = (3, 3) # in pixels fiducial_world = (10, 0) # in deg pscale = (0.000028, 0.000028) # in deg/pixel - dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs @@ -176,17 +175,6 @@ def test_wcs_from_footprints(): fiducial_world[1] - 0.000028, ) dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) - wcs_2 = dm_2.meta.wcs - - wcs = wcs_from_footprints([dm_1, dm_2]) - - # check that all elements of footprint match the *vertices* of the new combined WCS - assert all(np.isclose(wcs.footprint()[0], wcs(0, 0))) - assert all(np.isclose(wcs.footprint()[1], wcs(0, 4))) - assert all(np.isclose(wcs.footprint()[2], wcs(4, 4))) - assert all(np.isclose(wcs.footprint()[3], wcs(4, 0))) - - # check that fiducials match their expected coords in the new combined WCS assert all(np.isclose(wcs_1(0, 0), wcs(2.5, 1.5))) assert all(np.isclose(wcs_2(0, 0), wcs(3.5, 0.5))) From e23812048e0df2044215bdfbc54fbf55fb733852 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Mon, 10 Jul 2023 11:18:06 -0400 Subject: [PATCH 077/117] Rename variables with FITS keywords names. --- src/stcal/alignment/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index b8a556ad..3fac0fd8 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -596,8 +596,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - crpix=None, - crval=None, + ref_pixel=None, + ref_coord=None, ): """ Create a WCS from a list of input datamodels. From a26f04646fc45da515209d6904961049f7ff658d Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Wed, 12 Jul 2023 10:50:34 -0400 Subject: [PATCH 078/117] Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. --- src/stcal/alignment/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 3fac0fd8..b8a556ad 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -596,8 +596,8 @@ def wcs_from_footprints( pscale=None, rotation=None, shape=None, - ref_pixel=None, - ref_coord=None, + crpix=None, + crval=None, ): """ Create a WCS from a list of input datamodels. From 97dccaf14b7aaa652267b91e5d8bbe11703f0774 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 11:39:23 -0400 Subject: [PATCH 079/117] Code refactoring and additional unit test. --- src/stcal/alignment/util.py | 231 ++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index b8a556ad..83eb301b 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -587,6 +587,237 @@ def calc_rotation_matrix( return [pc1_1, pc1_2, pc2_1, pc2_2] +def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): + """ + Calculates axis mininum values and bounding box. + + Parameters + ---------- + ref_model : a valid datamodel + The reference datamodel for which to determine the minimum axis values and + bounding box. + + wcs_list : list + The list of WCS objects. + + ref_wcs : `~gwcs.wcs.WCS` + The reference WCS object. + + Returns + ------- + tuple + A tuple containing two elements: + 1 - a `numpy.array` with the minimum value in each axis; + 2 - a tuple containing the bounding box region in the format + ((x0_lower, x0_upper), (x1_lower, x1_upper)). + """ + footprints = [w.footprint().T for w in wcs_list] + domain_bounds = np.hstack( + [ref_wcs.backward_transform(*f) for f in footprints] + ) + axis_min_values = np.min(domain_bounds, axis=1) + domain_bounds = (domain_bounds.T - axis_min_values).T + + output_bounding_box = [] + for axis in ref_model.meta.wcs.output_frame.axes_order: + axis_min, axis_max = ( + domain_bounds[axis].min(), + domain_bounds[axis].max(), + ) + # populate output_bounding_box + output_bounding_box.append((axis_min, axis_max)) + + output_bounding_box = tuple(output_bounding_box) + return (axis_min_values, output_bounding_box) + + +def _calculate_fiducial(wcs_list, bounding_box, crval=None): + """ + Calculates the coordinates of the fiducial point and, if necessary, updates it with + the values in CRVAL (the update is applied to spatial axes only). + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + bounding_box : tuple, or list, optional + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + crval : list, optional + A reference world coordinate associated with the reference pixel. If not `None`, + then the fiducial coordinates of the spatial axes will be updated with the + values from `crval`. + + Returns + ------- + fiducial : `~numpy.ndarray` + A two-elements array containing the world coordinate of the fiducial point. + """ + fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) + if crval is not None: + i = 0 + for k, axt in enumerate(wcs_list[0].output_frame.axes_type): + if axt == "SPATIAL": + # overwrite only spatial axes with user-provided CRVAL + fiducial[k] = crval[i] + i += 1 + return fiducial + + +def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): + """ + Calculates the offsets to the transform. + + Parameters + ---------- + fiducial : `~numpy.ndarray` + A two-elements containing the world coordinates of the fiducial point. + + wcs : `~gwcs.wcs.WCS` + A WCS object. It will be used to determine the + + axis_min_values : `~numpy.ndarray` + A two-elements array containing the minimum pixel value for each axis. + + crpix : list or tuple + Pixel coordinates of the reference pixel. + + Returns + ------- + `~astropy.modeling.Model` + A model with the offsets to be added to the WCS's transform. + + Notes + ----- + If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. + The reason being that, in this case, the offsets will be calculated using the + WCS object to find the pixel coordinates of the fiducial point and then correct it + by the minimum pixel value for each axis. + """ + if ( + crpix is None + and fiducial is not None + and wcs is not None + and axis_min_values is not None + ): + offset1, offset2 = wcs.backward_transform(*fiducial) + offset1 -= axis_min_values[0] + offset2 -= axis_min_values[1] + else: + offset1, offset2 = crpix + + return astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( + -offset2, name="crpix2" + ) + + +def _calculate_new_wcs( + ref_model, shape, wcs_list, fiducial, crpix=None, transform=None +): + """ + Calculates a new WCS object based on the combined WCS objects provided. + + Parameters + ---------- + ref_model : a valid datamodel + The reference model to be used when extracting metadata. + + shape : list + The shape of the new WCS's pixel grid. If `None`, then the output bounding box + will be used to determine it. + + wcs_list : list + A list containing WCS objects. + + fiducial : `~numpy.ndarray` + A two-elements array containing the location on the sky in some standard + coordinate system. + + crpix : tuple, optional + The coordinates of the reference pixel. + + transform : `~astropy.modeling.Model`, optional + An optional tranform to be prepended to the transform constructed by the + fiducial point. The number of outputs of this transform must equal the number + of axes in the coordinate frame. + + Returns + ------- + `~gwcs.wcs.WCS` + The new WCS object that corresponds to the combined WCS objects in `wcs_list`. + """ + wcs_new = wcs_from_fiducial( + fiducial, + coordinate_frame=ref_model.meta.wcs.output_frame, + projection=astmodels.Pix2Sky_TAN(), + transform=transform, + input_frame=ref_model.meta.wcs.input_frame, + ) + axis_min_values, output_bounding_box = _get_axis_min_and_bounding_box( + ref_model, wcs_list, wcs_new + ) + offsets = _calculate_offsets( + fiducial=fiducial, + wcs=wcs_new, + axis_min_values=axis_min_values, + crpix=crpix, + ) + + wcs_new.insert_transform("detector", offsets, after=True) + wcs_new.bounding_box = output_bounding_box + + if shape is None: + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] + + wcs_new.pixel_shape = shape[::-1] + wcs_new.array_shape = shape + return wcs_new + + +def _validate_wcs_list(wcs_list): + """ + Validates wcs_list. + + Parameters + ---------- + wcs_list : list + A list of WCS objects. + + Returns + ------- + bool or Exception + If wcs_list is valid, returns True. Otherwise, it will raise an error. + + Raises + ------ + ValueError + Raised whenever wcs_list is not an iterable. + TypeError + Raised whenever wcs_list is empty or any of its content is not an + instance of WCS. + """ + if not isiterable(wcs_list): + raise ValueError( + "Expected 'wcs_list' to be an iterable of WCS objects." + ) + elif len(wcs_list): + if not all(isinstance(w, WCS) for w in wcs_list): + raise TypeError( + "All items in 'wcs_list' are to be instances of gwcs.WCS." + ) + else: + raise TypeError("'wcs_list' should not be empty.") + + return True + + def wcs_from_footprints( dmodels, refmodel=None, From 578fac376f5b1fc5c35375a49e77b43e0ee093be Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 13 Jul 2023 16:18:36 -0400 Subject: [PATCH 080/117] Bump astropy version to >= 5.1. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2da58d8f..60baa55d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.0.4', + 'astropy >=5.1', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', From 2a46a49e8d8e7629ad1e8a7adcafc1128637c090 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 14 Jul 2023 12:27:03 -0400 Subject: [PATCH 081/117] Fix docs style issues. --- pyproject.toml | 2 +- src/stcal/alignment/util.py | 240 +++++++++++++++++++++++++++++++++--- 2 files changed, 223 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 60baa55d..2da58d8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ 'Programming Language :: Python :: 3', ] dependencies = [ - 'astropy >=5.1', + 'astropy >=5.0.4', 'scipy >=1.6.0', 'numpy >=1.20', 'opencv-python-headless >=4.6.0.66', diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 83eb301b..45e9a7cc 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -593,21 +593,21 @@ def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference datamodel for which to determine the minimum axis values and bounding box. wcs_list : list The list of WCS objects. - ref_wcs : `~gwcs.wcs.WCS` + ref_wcs : ~gwcs.wcs.WCS The reference WCS object. Returns ------- tuple A tuple containing two elements: - 1 - a `numpy.array` with the minimum value in each axis; + 1 - a :py:class:`numpy.ndarray` with the minimum value in each axis; 2 - a tuple containing the bounding box region in the format ((x0_lower, x0_upper), (x1_lower, x1_upper)). """ @@ -651,11 +651,11 @@ def _calculate_fiducial(wcs_list, bounding_box, crval=None): crval : list, optional A reference world coordinate associated with the reference pixel. If not `None`, then the fiducial coordinates of the spatial axes will be updated with the - values from `crval`. + values from ``crval``. Returns ------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the world coordinate of the fiducial point. """ fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) @@ -675,13 +675,13 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Parameters ---------- - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements containing the world coordinates of the fiducial point. - wcs : `~gwcs.wcs.WCS` + wcs : ~gwcs.wcs.WCS A WCS object. It will be used to determine the - axis_min_values : `~numpy.ndarray` + axis_min_values : numpy.ndarray A two-elements array containing the minimum pixel value for each axis. crpix : list or tuple @@ -689,15 +689,15 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): Returns ------- - `~astropy.modeling.Model` + ~astropy.modeling.Model A model with the offsets to be added to the WCS's transform. Notes ----- - If `crpix=None`, then `fiducial`, `wcs`, and `axis_min_values` must be provided. - The reason being that, in this case, the offsets will be calculated using the - WCS object to find the pixel coordinates of the fiducial point and then correct it - by the minimum pixel value for each axis. + If ``crpix=None``, then ``fiducial``, ``wcs``, and ``axis_min_values`` must be + provided, in which case, the offsets will be calculated using the WCS object to + find the pixel coordinates of the fiducial point and then correct it by the minimum + pixel value for each axis. """ if ( crpix is None @@ -724,7 +724,7 @@ def _calculate_new_wcs( Parameters ---------- - ref_model : a valid datamodel + ref_model : The reference model to be used when extracting metadata. shape : list @@ -734,21 +734,21 @@ def _calculate_new_wcs( wcs_list : list A list containing WCS objects. - fiducial : `~numpy.ndarray` + fiducial : numpy.ndarray A two-elements array containing the location on the sky in some standard coordinate system. crpix : tuple, optional The coordinates of the reference pixel. - transform : `~astropy.modeling.Model`, optional + transform : ~astropy.modeling.Model An optional tranform to be prepended to the transform constructed by the fiducial point. The number of outputs of this transform must equal the number of axes in the coordinate frame. Returns ------- - `~gwcs.wcs.WCS` + wcs_new : ~gwcs.wcs.WCS The new WCS object that corresponds to the combined WCS objects in `wcs_list`. """ wcs_new = wcs_from_fiducial( @@ -810,7 +810,7 @@ def _validate_wcs_list(wcs_list): elif len(wcs_list): if not all(isinstance(w, WCS) for w in wcs_list): raise TypeError( - "All items in 'wcs_list' are to be instances of gwcs.WCS." + "All items in 'wcs_list' are to be instances of gwcs.wcs.WCS." ) else: raise TypeError("'wcs_list' should not be empty.") @@ -818,6 +818,210 @@ def _validate_wcs_list(wcs_list): return True +def wcsinfo_from_model(input_model: SupportsDataWithWcs): + """ + Creates a dict {wcs_keyword: array_of_values} pairs from a datamodel. + + Parameters + ---------- + input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + The input datamodel. + + Returns + ------- + wcsinfo : dict + A dict containing the WCS FITS keywords and corresponding values. + + """ + defaults = { + "CRPIX": 0, + "CRVAL": 0, + "CDELT": 1.0, + "CTYPE": "", + "CUNIT": u.Unit(""), + } + wcsaxes = input_model.meta.wcsinfo.wcsaxes + wcsinfo = {"WCSAXES": wcsaxes} + for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: + val = [] + for ax in range(1, wcsaxes + 1): + k = (key + "{0}".format(ax)).lower() + v = getattr(input_model.meta.wcsinfo, k, defaults[key]) + val.append(v) + wcsinfo[key] = np.array(val) + + pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) + for i in range(1, wcsaxes + 1): + for j in range(1, wcsaxes + 1): + pc[i - 1, j - 1] = getattr( + input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 + ) + wcsinfo["PC"] = pc + wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame + wcsinfo["has_cd"] = False + return wcsinfo + + +def compute_scale( + wcs: WCS, + fiducial: Union[tuple, np.ndarray], + disp_axis: int = None, + pscale_ratio: float = None, +) -> float: + """Compute scaling transform. + + Parameters + ---------- + wcs : ~gwcs.wcs.WCS + Reference WCS object from which to compute a scaling factor. + + fiducial : tuple + Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating + reference points. + + disp_axis : int + Dispersion axis integer. Assumes the same convention as + ``wcsinfo.dispersion_direction`` + + pscale_ratio : int + Ratio of input to output pixel scale + + Returns + ------- + scale : float + Scaling factor for x and y or cross-dispersion direction. + + """ + spectral = "SPECTRAL" in wcs.output_frame.axes_type + + if spectral and disp_axis is None: + raise ValueError("If input WCS is spectral, a disp_axis must be given") + + crpix = np.array(wcs.invert(*fiducial)) + + delta = np.zeros_like(crpix) + spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] + delta[spatial_idx[0]] = 1 + + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T + crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) + + coords = SkyCoord( + ra=crval_with_offsets[spatial_idx[0]], + dec=crval_with_offsets[spatial_idx[1]], + unit="deg", + ) + xscale = np.abs(coords[0].separation(coords[1]).value) + yscale = np.abs(coords[0].separation(coords[2]).value) + + if pscale_ratio is not None: + xscale *= pscale_ratio + yscale *= pscale_ratio + + if spectral: + # Assuming scale doesn't change with wavelength + # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction + return yscale if disp_axis == 1 else xscale + + return np.sqrt(xscale * yscale) + + +def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: + """ + Calculates the world coordinates of the fiducial point of a list of WCS objects. + For a celestial footprint this is the center. For a spectral footprint, it is the + beginning of its range. + + Parameters + ---------- + wcslist : list + A list containing all the WCS objects for which the fiducial is to be + calculated. + + bounding_box : tuple, list, None + The bounding box over which the WCS is valid. It can be a either tuple of tuples + or a list of lists of size 2 where each element represents a range of + (low, high) values. The bounding_box is in the order of the axes, axes_order. + For two inputs and axes_order(0, 1) the bounding box can be either + ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. + + Returns + ------- + fiducial : numpy.ndarray + A two-elements array containing the world coordinates of the fiducial point + in the combined output coordinate frame. + + Notes + ----- + This function assumes all WCSs have the same output coordinate frame. + """ + + axes_types = wcslist[0].output_frame.axes_type + spatial_axes = np.array(axes_types) == "SPATIAL" + spectral_axes = np.array(axes_types) == "SPECTRAL" + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) + spatial_footprint = footprints[spatial_axes] + spectral_footprint = footprints[spectral_axes] + + fiducial = np.empty(len(axes_types)) + if spatial_footprint.any(): + fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( + spatial_footprint + ) + if spectral_footprint.any(): + fiducial[spectral_axes] = spectral_footprint.min() + return fiducial + + +def calc_rotation_matrix( + roll_ref: float, v3i_yangle: float, vparity: int = 1 +) -> List[float]: + """Calculate the rotation matrix. + + Parameters + ---------- + roll_ref : float + Telescope roll angle of V3 North over East at the ref. point in radians + + v3i_yangle : float + The angle between ideal Y-axis and V3 in radians. + + vparity : int + The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. + Value should be "1" or "-1". + + Returns + ------- + matrix: list + A list containing the rotation matrix elements in column order. + + Notes + ----- + The rotation matrix is + + .. math:: + PC = \\begin{bmatrix} + pc_{1,1} & pc_{2,1} \\\\ + pc_{1,2} & pc_{2,2} + \\end{bmatrix} + """ + if vparity not in (1, -1): + raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") + + rel_angle = roll_ref - (vparity * v3i_yangle) + + pc1_1 = vparity * np.cos(rel_angle) + pc1_2 = np.sin(rel_angle) + pc2_1 = vparity * -np.sin(rel_angle) + pc2_2 = np.cos(rel_angle) + + return [pc1_1, pc1_2, pc2_1, pc2_2] + + def wcs_from_footprints( dmodels, refmodel=None, From 3f31c44e96328ca6990ab40d3219cf1adfba269a Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 17 Aug 2023 10:46:46 -0400 Subject: [PATCH 082/117] Small refactoring and clean ups. --- src/stcal/alignment/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 45e9a7cc..776937eb 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -824,7 +824,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): Parameters ---------- - input_model : ~stdatamodels.jwst.datamodels.JwstDataModel + input_model : The input datamodel. Returns From 759f7966c4f01e0a268c69d50cd4354c3cf05ddd Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 11:45:42 -0400 Subject: [PATCH 083/117] Fix style issues. --- src/stcal/alignment/reproject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index daf9a786..3c1af6f9 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -30,7 +30,7 @@ def _get_forward_transform_func(wcs1): y (str, ndarray), and origin (int). The origin should be between 0, and 1 https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) - """ + """ # noqa : E501 if isinstance(wcs1, fitswcs.WCS): forward_transform = wcs1.all_pix2world elif isinstance(wcs1, gwcs.WCS): From 9ac8694402f3852e87028b9084c6d4048b78b772 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 12:00:27 -0400 Subject: [PATCH 084/117] Revert reproject commit. --- src/stcal/alignment/reproject.py | 89 --------------------- src/stcal/alignment/tests/test_reproject.py | 59 -------------- 2 files changed, 148 deletions(-) delete mode 100644 src/stcal/alignment/reproject.py delete mode 100644 src/stcal/alignment/tests/test_reproject.py diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py deleted file mode 100644 index 3c1af6f9..00000000 --- a/src/stcal/alignment/reproject.py +++ /dev/null @@ -1,89 +0,0 @@ -import gwcs -import numpy as np -from astropy import wcs as fitswcs -from astropy.modeling import Model -from typing import Union - - -def reproject_coords(wcs1, wcs2): - """ - Given two WCSs or transforms return a function which takes pixel - coordinates in the first WCS or transform and computes them in pixel coordinates - in the second one. It performs the forward transformation of ``wcs1`` followed by the - inverse of ``wcs2``. - - Parameters - ---------- - wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` - WCS objects. - - Returns - ------- - _reproject : func - Function to compute the transformations. It takes x, y - positions in ``wcs1`` and returns x, y positions in ``wcs2``. - """ - - def _get_forward_transform_func(wcs1): - """Get the forward transform function from the input WCS. If the wcs is a - fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), - y (str, ndarray), and origin (int). The origin should be between 0, and 1 - https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file - ) - """ # noqa : E501 - if isinstance(wcs1, fitswcs.WCS): - forward_transform = wcs1.all_pix2world - elif isinstance(wcs1, gwcs.WCS): - forward_transform = wcs1.forward_transform - elif issubclass(wcs1, Model): - forward_transform = wcs1 - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return forward_transform - - def _get_backward_transform_func(wcs2): - if isinstance(wcs2, fitswcs.WCS): - backward_transform = wcs2.all_world2pix - elif isinstance(wcs2, gwcs.WCS): - backward_transform = wcs2.backward_transform - elif issubclass(wcs2, Model): - backward_transform = wcs2.inverse - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return backward_transform - - def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: - """ - Reprojects the input coordinates from one WCS to another. - - Parameters: - ----------- - x : str or np.ndarray - Array of x-coordinates to be reprojected. - y : str or np.ndarray - Array of y-coordinates to be reprojected. - - Returns: - -------- - tuple - Tuple of reprojected x and y coordinates. - """ - # example inputs to resulting function (12, 13, 0) # third number is origin - if not isinstance(x, list): - x = [x] - if not isinstance(y, list): - y = [y] - if len(x) != len(y): - raise ValueError("x and y must be the same length") - sky = _get_forward_transform_func(wcs1)(x, y, 0) - sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) - new_sky = tuple(sky_back[:, :1].flatten()) - return tuple(new_sky) - - return _reproject diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py deleted file mode 100644 index 72cd8e8a..00000000 --- a/src/stcal/alignment/tests/test_reproject.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest -import numpy as np -from astropy.io import fits -from astropy.wcs import WCS -from stcal.alignment import reproject - - -def get_fake_wcs(): - fake_wcs1 = WCS( - fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.1, - "CDELT2": 0.1, - } - ) - ) - fake_wcs2 = WCS( - fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.05, - "CDELT2": 0.05, - } - ) - ) - return fake_wcs1, fake_wcs2 - - -@pytest.mark.parametrize( - "x_inp, y_inp, x_expected, y_expected", - [ - (1000, 2000, 2000, 4000), # string input test - ([1000], [2000], 2000, 4000), # array input test - pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test - ], -) -def test__reproject(x_inp, y_inp, x_expected, y_expected): - wcs1, wcs2 = get_fake_wcs() - f = reproject.reproject_coords(wcs1, wcs2) - x_out, y_out = f(x_inp, y_inp) - assert np.allclose(x_out, x_expected, rtol=1e-05) - assert np.allclose(y_out, y_expected, rtol=1e-05) From 220656f3485b079ae9a2ec5e9d5389911b6b8fbf Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 16:15:12 -0400 Subject: [PATCH 085/117] Revert "Revert reproject commit." This reverts commit 9ac8694402f3852e87028b9084c6d4048b78b772. --- src/stcal/alignment/reproject.py | 89 +++++++++++++++++++++ src/stcal/alignment/tests/test_reproject.py | 59 ++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/stcal/alignment/reproject.py create mode 100644 src/stcal/alignment/tests/test_reproject.py diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py new file mode 100644 index 00000000..3c1af6f9 --- /dev/null +++ b/src/stcal/alignment/reproject.py @@ -0,0 +1,89 @@ +import gwcs +import numpy as np +from astropy import wcs as fitswcs +from astropy.modeling import Model +from typing import Union + + +def reproject_coords(wcs1, wcs2): + """ + Given two WCSs or transforms return a function which takes pixel + coordinates in the first WCS or transform and computes them in pixel coordinates + in the second one. It performs the forward transformation of ``wcs1`` followed by the + inverse of ``wcs2``. + + Parameters + ---------- + wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + WCS objects. + + Returns + ------- + _reproject : func + Function to compute the transformations. It takes x, y + positions in ``wcs1`` and returns x, y positions in ``wcs2``. + """ + + def _get_forward_transform_func(wcs1): + """Get the forward transform function from the input WCS. If the wcs is a + fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), + y (str, ndarray), and origin (int). The origin should be between 0, and 1 + https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file + ) + """ # noqa : E501 + if isinstance(wcs1, fitswcs.WCS): + forward_transform = wcs1.all_pix2world + elif isinstance(wcs1, gwcs.WCS): + forward_transform = wcs1.forward_transform + elif issubclass(wcs1, Model): + forward_transform = wcs1 + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return forward_transform + + def _get_backward_transform_func(wcs2): + if isinstance(wcs2, fitswcs.WCS): + backward_transform = wcs2.all_world2pix + elif isinstance(wcs2, gwcs.WCS): + backward_transform = wcs2.backward_transform + elif issubclass(wcs2, Model): + backward_transform = wcs2.inverse + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return backward_transform + + def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: + """ + Reprojects the input coordinates from one WCS to another. + + Parameters: + ----------- + x : str or np.ndarray + Array of x-coordinates to be reprojected. + y : str or np.ndarray + Array of y-coordinates to be reprojected. + + Returns: + -------- + tuple + Tuple of reprojected x and y coordinates. + """ + # example inputs to resulting function (12, 13, 0) # third number is origin + if not isinstance(x, list): + x = [x] + if not isinstance(y, list): + y = [y] + if len(x) != len(y): + raise ValueError("x and y must be the same length") + sky = _get_forward_transform_func(wcs1)(x, y, 0) + sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) + new_sky = tuple(sky_back[:, :1].flatten()) + return tuple(new_sky) + + return _reproject diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py new file mode 100644 index 00000000..72cd8e8a --- /dev/null +++ b/src/stcal/alignment/tests/test_reproject.py @@ -0,0 +1,59 @@ +import pytest +import numpy as np +from astropy.io import fits +from astropy.wcs import WCS +from stcal.alignment import reproject + + +def get_fake_wcs(): + fake_wcs1 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.1, + "CDELT2": 0.1, + } + ) + ) + fake_wcs2 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.05, + "CDELT2": 0.05, + } + ) + ) + return fake_wcs1, fake_wcs2 + + +@pytest.mark.parametrize( + "x_inp, y_inp, x_expected, y_expected", + [ + (1000, 2000, 2000, 4000), # string input test + ([1000], [2000], 2000, 4000), # array input test + pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test + ], +) +def test__reproject(x_inp, y_inp, x_expected, y_expected): + wcs1, wcs2 = get_fake_wcs() + f = reproject.reproject_coords(wcs1, wcs2) + x_out, y_out = f(x_inp, y_inp) + assert np.allclose(x_out, x_expected, rtol=1e-05) + assert np.allclose(y_out, y_expected, rtol=1e-05) From 97c798fb3702cf59df00e5a0925622973f015875 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 16:21:51 -0400 Subject: [PATCH 086/117] Fix issue caused by bad conflict resolution. --- tests/test_alignment.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 7d250f65..edfc5db1 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -175,6 +175,17 @@ def test_wcs_from_footprints(): fiducial_world[1] - 0.000028, ) dm_2 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) + wcs_2 = dm_2.meta.wcs + + wcs = wcs_from_footprints([dm_1, dm_2]) + + # check that all elements of footprint match the *vertices* of the new combined WCS + assert all(np.isclose(wcs.footprint()[0], wcs(0, 0))) + assert all(np.isclose(wcs.footprint()[1], wcs(0, 4))) + assert all(np.isclose(wcs.footprint()[2], wcs(4, 4))) + assert all(np.isclose(wcs.footprint()[3], wcs(4, 0))) + + # check that fiducials match their expected coords in the new combined WCS assert all(np.isclose(wcs_1(0, 0), wcs(2.5, 1.5))) assert all(np.isclose(wcs_2(0, 0), wcs(3.5, 0.5))) From b42a922ce65bfc7c31c42ed23aab644f00dbde95 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Tue, 22 Aug 2023 16:31:16 -0400 Subject: [PATCH 087/117] Fix duplicate code. --- src/stcal/alignment/util.py | 437 +----------------------------------- 1 file changed, 1 insertion(+), 436 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 776937eb..1346f683 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -281,441 +281,6 @@ def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): ) -def _calculate_new_wcs( - ref_model, shape, wcs_list, fiducial, crpix=None, transform=None -): - """ - Calculates a new WCS object based on the combined WCS objects provided. - - Parameters - ---------- - ref_model : - The reference datamodel to be used when extracting metadata. - - shape : list - The shape of the new WCS's pixel grid. If `None`, then the output bounding box - will be used to determine it. - - wcs_list : list - A list containing WCS objects. - - fiducial : numpy.ndarray - A two-elements array containing the location on the sky in some standard - coordinate system. - - crpix : tuple, optional - The coordinates of the reference pixel. - - transform : ~astropy.modeling.Model - An optional tranform to be prepended to the transform constructed by the - fiducial point. The number of outputs of this transform must equal the number - of axes in the coordinate frame. - - Returns - ------- - wcs_new : ~gwcs.wcs.WCS - The new WCS object that corresponds to the combined WCS objects in `wcs_list`. - """ - wcs_new = wcs_from_fiducial( - fiducial, - coordinate_frame=ref_model.meta.wcs.output_frame, - projection=astmodels.Pix2Sky_TAN(), - transform=transform, - input_frame=ref_model.meta.wcs.input_frame, - ) - axis_min_values, output_bounding_box = _get_axis_min_and_bounding_box( - ref_model, wcs_list, wcs_new - ) - offsets = _calculate_offsets( - fiducial=fiducial, - wcs=wcs_new, - axis_min_values=axis_min_values, - crpix=crpix, - ) - - wcs_new.insert_transform("detector", offsets, after=True) - wcs_new.bounding_box = output_bounding_box - - if shape is None: - shape = [ - int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] - ] - - wcs_new.pixel_shape = shape[::-1] - wcs_new.array_shape = shape - return wcs_new - - -def _validate_wcs_list(wcs_list): - """ - Validates wcs_list. - - Parameters - ---------- - wcs_list : list - A list of WCS objects. - - Returns - ------- - bool or Exception - If wcs_list is valid, returns True. Otherwise, it will raise an error. - - Raises - ------ - ValueError - Raised whenever wcs_list is not an iterable. - TypeError - Raised whenever wcs_list is empty or any of its content is not an - instance of WCS. - """ - if not isiterable(wcs_list): - raise ValueError( - "Expected 'wcs_list' to be an iterable of WCS objects." - ) - elif len(wcs_list): - if not all(isinstance(w, WCS) for w in wcs_list): - raise TypeError( - "All items in 'wcs_list' are to be instances of gwcs.wcs.WCS." - ) - else: - raise TypeError("'wcs_list' should not be empty.") - - return True - - -def wcsinfo_from_model(input_model: SupportsDataWithWcs): - """ - Creates a dict {wcs_keyword: array_of_values} pairs from a datamodel. - - Parameters - ---------- - input_model : - The input datamodel. - - Returns - ------- - wcsinfo : dict - A dict containing the WCS FITS keywords and corresponding values. - - """ - defaults = { - "CRPIX": 0, - "CRVAL": 0, - "CDELT": 1.0, - "CTYPE": "", - "CUNIT": u.Unit(""), - } - wcsaxes = input_model.meta.wcsinfo.wcsaxes - wcsinfo = {"WCSAXES": wcsaxes} - for key in ["CRPIX", "CRVAL", "CDELT", "CTYPE", "CUNIT"]: - val = [] - for ax in range(1, wcsaxes + 1): - k = (key + "{0}".format(ax)).lower() - v = getattr(input_model.meta.wcsinfo, k, defaults[key]) - val.append(v) - wcsinfo[key] = np.array(val) - - pc = np.zeros((wcsaxes, wcsaxes), dtype=np.float32) - for i in range(1, wcsaxes + 1): - for j in range(1, wcsaxes + 1): - pc[i - 1, j - 1] = getattr( - input_model.meta.wcsinfo, "pc{0}_{1}".format(i, j), 1 - ) - wcsinfo["PC"] = pc - wcsinfo["RADESYS"] = input_model.meta.coordinates.reference_frame - wcsinfo["has_cd"] = False - return wcsinfo - - -def compute_scale( - wcs: WCS, - fiducial: Union[tuple, np.ndarray], - disp_axis: int = None, - pscale_ratio: float = None, -) -> float: - """Compute scaling transform. - - Parameters - ---------- - wcs : ~gwcs.wcs.WCS - Reference WCS object from which to compute a scaling factor. - - fiducial : tuple - Input fiducial of (RA, DEC) or (RA, DEC, Wavelength) used in calculating - reference points. - - disp_axis : int - Dispersion axis integer. Assumes the same convention as - ``wcsinfo.dispersion_direction`` - - pscale_ratio : int - Ratio of input to output pixel scale - - Returns - ------- - scale : float - Scaling factor for x and y or cross-dispersion direction. - - """ - spectral = "SPECTRAL" in wcs.output_frame.axes_type - - if spectral and disp_axis is None: - raise ValueError("If input WCS is spectral, a disp_axis must be given") - - crpix = np.array(wcs.invert(*fiducial)) - - delta = np.zeros_like(crpix) - spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] - delta[spatial_idx[0]] = 1 - - crpix_with_offsets = np.vstack( - (crpix, crpix + delta, crpix + np.roll(delta, 1)) - ).T - crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) - - coords = SkyCoord( - ra=crval_with_offsets[spatial_idx[0]], - dec=crval_with_offsets[spatial_idx[1]], - unit="deg", - ) - xscale = np.abs(coords[0].separation(coords[1]).value) - yscale = np.abs(coords[0].separation(coords[2]).value) - - if pscale_ratio is not None: - xscale *= pscale_ratio - yscale *= pscale_ratio - - if spectral: - # Assuming scale doesn't change with wavelength - # Assuming disp_axis is consistent with DataModel.meta.wcsinfo.dispersion.direction - return yscale if disp_axis == 1 else xscale - - return np.sqrt(xscale * yscale) - - -def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: - """ - Calculates the world coordinates of the fiducial point of a list of WCS objects. - For a celestial footprint this is the center. For a spectral footprint, it is the - beginning of its range. - - Parameters - ---------- - wcslist : list - A list containing all the WCS objects for which the fiducial is to be - calculated. - - bounding_box : tuple, list, None - The bounding box over which the WCS is valid. It can be a either tuple of tuples - or a list of lists of size 2 where each element represents a range of - (low, high) values. The bounding_box is in the order of the axes, axes_order. - For two inputs and axes_order(0, 1) the bounding box can be either - ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. - - Returns - ------- - fiducial : numpy.ndarray - A two-elements array containing the world coordinates of the fiducial point - in the combined output coordinate frame. - - Notes - ----- - This function assumes all WCSs have the same output coordinate frame. - """ - - axes_types = wcslist[0].output_frame.axes_type - spatial_axes = np.array(axes_types) == "SPATIAL" - spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack( - [w.footprint(bounding_box=bounding_box).T for w in wcslist] - ) - spatial_footprint = footprints[spatial_axes] - spectral_footprint = footprints[spectral_axes] - - fiducial = np.empty(len(axes_types)) - if spatial_footprint.any(): - fiducial[spatial_axes] = _calculate_fiducial_from_spatial_footprint( - spatial_footprint - ) - if spectral_footprint.any(): - fiducial[spectral_axes] = spectral_footprint.min() - return fiducial - - -def calc_rotation_matrix( - roll_ref: float, v3i_yangle: float, vparity: int = 1 -) -> List[float]: - """Calculate the rotation matrix. - - Parameters - ---------- - roll_ref : float - Telescope roll angle of V3 North over East at the ref. point in radians - - v3i_yangle : float - The angle between ideal Y-axis and V3 in radians. - - vparity : int - The x-axis parity, usually taken from the JWST SIAF parameter VIdlParity. - Value should be "1" or "-1". - - Returns - ------- - matrix: list - A list containing the rotation matrix elements in column order. - - Notes - ----- - The rotation matrix is - - .. math:: - PC = \\begin{bmatrix} - pc_{1,1} & pc_{2,1} \\\\ - pc_{1,2} & pc_{2,2} - \\end{bmatrix} - """ - if vparity not in (1, -1): - raise ValueError(f"vparity should be 1 or -1. Input was: {vparity}") - - rel_angle = roll_ref - (vparity * v3i_yangle) - - pc1_1 = vparity * np.cos(rel_angle) - pc1_2 = np.sin(rel_angle) - pc2_1 = vparity * -np.sin(rel_angle) - pc2_2 = np.cos(rel_angle) - - return [pc1_1, pc1_2, pc2_1, pc2_2] - - -def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): - """ - Calculates axis mininum values and bounding box. - - Parameters - ---------- - ref_model : - The reference datamodel for which to determine the minimum axis values and - bounding box. - - wcs_list : list - The list of WCS objects. - - ref_wcs : ~gwcs.wcs.WCS - The reference WCS object. - - Returns - ------- - tuple - A tuple containing two elements: - 1 - a :py:class:`numpy.ndarray` with the minimum value in each axis; - 2 - a tuple containing the bounding box region in the format - ((x0_lower, x0_upper), (x1_lower, x1_upper)). - """ - footprints = [w.footprint().T for w in wcs_list] - domain_bounds = np.hstack( - [ref_wcs.backward_transform(*f) for f in footprints] - ) - axis_min_values = np.min(domain_bounds, axis=1) - domain_bounds = (domain_bounds.T - axis_min_values).T - - output_bounding_box = [] - for axis in ref_model.meta.wcs.output_frame.axes_order: - axis_min, axis_max = ( - domain_bounds[axis].min(), - domain_bounds[axis].max(), - ) - # populate output_bounding_box - output_bounding_box.append((axis_min, axis_max)) - - output_bounding_box = tuple(output_bounding_box) - return (axis_min_values, output_bounding_box) - - -def _calculate_fiducial(wcs_list, bounding_box, crval=None): - """ - Calculates the coordinates of the fiducial point and, if necessary, updates it with - the values in CRVAL (the update is applied to spatial axes only). - - Parameters - ---------- - wcs_list : list - A list of WCS objects. - - bounding_box : tuple, or list, optional - The bounding box over which the WCS is valid. It can be a either tuple of tuples - or a list of lists of size 2 where each element represents a range of - (low, high) values. The bounding_box is in the order of the axes, axes_order. - For two inputs and axes_order(0, 1) the bounding box can be either - ((xlow, xhigh), (ylow, yhigh)) or [[xlow, xhigh], [ylow, yhigh]]. - - crval : list, optional - A reference world coordinate associated with the reference pixel. If not `None`, - then the fiducial coordinates of the spatial axes will be updated with the - values from ``crval``. - - Returns - ------- - fiducial : numpy.ndarray - A two-elements array containing the world coordinate of the fiducial point. - """ - fiducial = compute_fiducial(wcs_list, bounding_box=bounding_box) - if crval is not None: - i = 0 - for k, axt in enumerate(wcs_list[0].output_frame.axes_type): - if axt == "SPATIAL": - # overwrite only spatial axes with user-provided CRVAL - fiducial[k] = crval[i] - i += 1 - return fiducial - - -def _calculate_offsets(fiducial, wcs, axis_min_values, crpix): - """ - Calculates the offsets to the transform. - - Parameters - ---------- - fiducial : numpy.ndarray - A two-elements containing the world coordinates of the fiducial point. - - wcs : ~gwcs.wcs.WCS - A WCS object. It will be used to determine the - - axis_min_values : numpy.ndarray - A two-elements array containing the minimum pixel value for each axis. - - crpix : list or tuple - Pixel coordinates of the reference pixel. - - Returns - ------- - ~astropy.modeling.Model - A model with the offsets to be added to the WCS's transform. - - Notes - ----- - If ``crpix=None``, then ``fiducial``, ``wcs``, and ``axis_min_values`` must be - provided, in which case, the offsets will be calculated using the WCS object to - find the pixel coordinates of the fiducial point and then correct it by the minimum - pixel value for each axis. - """ - if ( - crpix is None - and fiducial is not None - and wcs is not None - and axis_min_values is not None - ): - offset1, offset2 = wcs.backward_transform(*fiducial) - offset1 -= axis_min_values[0] - offset2 -= axis_min_values[1] - else: - offset1, offset2 = crpix - - return astmodels.Shift(-offset1, name="crpix1") & astmodels.Shift( - -offset2, name="crpix2" - ) - - def _calculate_new_wcs( ref_model, shape, wcs_list, fiducial, crpix=None, transform=None ): @@ -1002,7 +567,7 @@ def calc_rotation_matrix( Notes ----- The rotation matrix is - + .. math:: PC = \\begin{bmatrix} pc_{1,1} & pc_{2,1} \\\\ From 3771110b873c2d103e2ca82efe8efbb83589d72e Mon Sep 17 00:00:00 2001 From: mairan Date: Wed, 23 Aug 2023 10:02:26 -0400 Subject: [PATCH 088/117] Style check fixes. (#194) * Add methods necessary for resample. * Small changes to accommodate both JWST and RST datamodels. * Add CHANGES.rst entry. * Add PR number to CHANGES.rst entry. * use protocols * temp * add dependencies * add CI testing to alignment branch * add CI testing to alignment branch * fix test * Add setting of number_extended_events (#178) * Add setting of number_extended_events This was not being set when in single processing mode. * Update CHANGES.rst * Update CHANGES.rst * Update test_jump.py --------- Co-authored-by: Howard Bushouse * Rename variables with FITS keywords names. * Style reformating. * Update docs to include stcal.alignment. * Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. * Updates to address most comments. * Fix misplaced comment. * Code refactoring and additional unit test. * Silencing style check warning F403. * Small style refactoring. * Set minimum asdf version. * Update matplotlib's inventory URL. * Set lower version for gwcs. * Bump astropy version to >= 5.1. * Fix docs style issues. * Style check fix. * Fix issue with bounding boxes in tests. * Fix issue with bounding boxes in tests. * Add new methods required by resample_step. * Small refactoring and clean ups. * RCAL-632: add unit tests for new methods. * Fix style check errors. * Set sphinx version for compatibility with sphinx-rtd-theme. * Fix style issues. * Revert reproject commit. * Small changes to accommodate both JWST and RST datamodels. * fix test * Rename variables with FITS keywords names. * Revert "Rename variables with FITS keywords names." This reverts commit 0eea0e5d4902286dd7b1966537dc648ef3f14cbf. * Code refactoring and additional unit test. * Bump astropy version to >= 5.1. * Fix docs style issues. * Small refactoring and clean ups. * Fix style issues. * Revert reproject commit. * Revert "Revert reproject commit." This reverts commit 9ac8694402f3852e87028b9084c6d4048b78b772. * Fix issue caused by bad conflict resolution. * Fix duplicate code. --------- Co-authored-by: Nadia Dencheva Co-authored-by: mwregan2 Co-authored-by: Howard Bushouse --- src/stcal/alignment/reproject.py | 2 +- src/stcal/alignment/util.py | 2 +- tests/test_alignment.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py index daf9a786..3c1af6f9 100644 --- a/src/stcal/alignment/reproject.py +++ b/src/stcal/alignment/reproject.py @@ -30,7 +30,7 @@ def _get_forward_transform_func(wcs1): y (str, ndarray), and origin (int). The origin should be between 0, and 1 https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) - """ + """ # noqa : E501 if isinstance(wcs1, fitswcs.WCS): forward_transform = wcs1.all_pix2world elif isinstance(wcs1, gwcs.WCS): diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index b8a556ad..1346f683 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -290,7 +290,7 @@ def _calculate_new_wcs( Parameters ---------- ref_model : - The reference datamodel to be used when extracting metadata. + The reference model to be used when extracting metadata. shape : list The shape of the new WCS's pixel grid. If `None`, then the output bounding box diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 2198990c..edfc5db1 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -8,7 +8,6 @@ from gwcs import coordinate_frames as cf import pytest - from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -166,7 +165,6 @@ def test_wcs_from_footprints(): shape = (3, 3) # in pixels fiducial_world = (10, 0) # in deg pscale = (0.000028, 0.000028) # in deg/pixel - dm_1 = _create_wcs_and_datamodel(fiducial_world, shape, pscale) wcs_1 = dm_1.meta.wcs From 3f326797f80b9706b52fd63247336519e01d9ed8 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:40:25 -0400 Subject: [PATCH 089/117] Moved test for alignment.reproject, remove ipynb. --- src/stcal/alignment/testing_reproject.ipynb | 79 --------------------- src/stcal/alignment/tests/test_reproject.py | 59 --------------- tests/test_alignment.py | 55 +++++++++++++- 3 files changed, 54 insertions(+), 139 deletions(-) delete mode 100644 src/stcal/alignment/testing_reproject.ipynb delete mode 100644 src/stcal/alignment/tests/test_reproject.py diff --git a/src/stcal/alignment/testing_reproject.ipynb b/src/stcal/alignment/testing_reproject.ipynb deleted file mode 100644 index b63f4c71..00000000 --- a/src/stcal/alignment/testing_reproject.ipynb +++ /dev/null @@ -1,79 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from reproject import reproject_coords\n", - "from stwcs.wcsutil import HSTWCS\n", - "from astropy.wcs import WCS\n", - "from astropy.io import fits\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(2999.9999999998668, 2999.9999999999286)" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hdu1=fits.open('hst_12546_22_acs_wfc_f606w_jbt122x1_drc.fits')\n", - "hdu2=fits.open('hst_12546_22_acs_wfc_f814w_jbt122_drc.fits')\n", - "wcs1=WCS(hdu1[1].header)\n", - "wcs2=WCS(hdu2[1].header)\n", - "# wcs2 = HSTWCS('jcdm74guq_drc.fits', ext=1)\n", - "f = reproject_coords(wcs1, wcs2)\n", - "f([3000], [3000])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/src/stcal/alignment/tests/test_reproject.py b/src/stcal/alignment/tests/test_reproject.py deleted file mode 100644 index 72cd8e8a..00000000 --- a/src/stcal/alignment/tests/test_reproject.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest -import numpy as np -from astropy.io import fits -from astropy.wcs import WCS -from stcal.alignment import reproject - - -def get_fake_wcs(): - fake_wcs1 = WCS( - fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.1, - "CDELT2": 0.1, - } - ) - ) - fake_wcs2 = WCS( - fits.Header( - { - "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, - "CTYPE1": "RA---TAN", - "CTYPE2": "DEC--TAN", - "CRVAL1": 0, - "CRVAL2": 0, - "CRPIX1": 1, - "CRPIX2": 1, - "CDELT1": -0.05, - "CDELT2": 0.05, - } - ) - ) - return fake_wcs1, fake_wcs2 - - -@pytest.mark.parametrize( - "x_inp, y_inp, x_expected, y_expected", - [ - (1000, 2000, 2000, 4000), # string input test - ([1000], [2000], 2000, 4000), # array input test - pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test - ], -) -def test__reproject(x_inp, y_inp, x_expected, y_expected): - wcs1, wcs2 = get_fake_wcs() - f = reproject.reproject_coords(wcs1, wcs2) - x_out, y_out = f(x_inp, y_inp) - assert np.allclose(x_out, x_expected, rtol=1e-05) - assert np.allclose(y_out, y_expected, rtol=1e-05) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 8888fcf0..271f3588 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -8,7 +8,7 @@ from gwcs import coordinate_frames as cf import pytest - +from stcal.alignment import reproject from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -213,3 +213,56 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): _validate_wcs_list(wcs_list) assert type(exec_info.value) == expected_error + +def get_fake_wcs(): + fake_wcs1 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.1, + "CDELT2": 0.1, + } + ) + ) + fake_wcs2 = WCS( + fits.Header( + { + "NAXIS": 2, + "NAXIS1": 1, + "NAXIS2": 1, + "CTYPE1": "RA---TAN", + "CTYPE2": "DEC--TAN", + "CRVAL1": 0, + "CRVAL2": 0, + "CRPIX1": 1, + "CRPIX2": 1, + "CDELT1": -0.05, + "CDELT2": 0.05, + } + ) + ) + return fake_wcs1, fake_wcs2 + + +@pytest.mark.parametrize( + "x_inp, y_inp, x_expected, y_expected", + [ + (1000, 2000, 2000, 4000), # string input test + ([1000], [2000], 2000, 4000), # array input test + pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test + ], +) +def test__reproject(x_inp, y_inp, x_expected, y_expected): + wcs1, wcs2 = get_fake_wcs() + f = reproject.reproject_coords(wcs1, wcs2) + x_out, y_out = f(x_inp, y_inp) + assert np.allclose(x_out, x_expected, rtol=1e-05) + assert np.allclose(y_out, y_expected, rtol=1e-05) From e0fad8c66fb5de397e571aee08afd43d764e8aa8 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 24 Aug 2023 09:46:09 -0400 Subject: [PATCH 090/117] Removed unnecessary file. --- src/stcal/alignment/resample_utils.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/stcal/alignment/resample_utils.py diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py deleted file mode 100644 index e69de29b..00000000 From 91093812fd8bdf0161559a98495b2c4013f8930e Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:00:34 -0400 Subject: [PATCH 091/117] Update test_alignment.py --- tests/test_alignment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index a8907cf9..b78d4301 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -281,8 +281,6 @@ def test__reproject(x_inp, y_inp, x_expected, y_expected): f = reproject.reproject_coords(wcs1, wcs2) x_out, y_out = f(x_inp, y_inp) assert np.allclose(x_out, x_expected, rtol=1e-05) - assert np.allclose(y_out, y_expected, rtol=1e-05) -======= @pytest.mark.parametrize( "model, footprint, expected_s_region, expected_log_info", From 6a75ddc3ad6575c74b1f65ab0eeafaeef5cc5258 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:03:24 -0400 Subject: [PATCH 092/117] Added astropy.io.fits --- tests/test_alignment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index b78d4301..4b222ab8 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -3,6 +3,7 @@ from astropy.modeling import models from astropy import coordinates as coord from astropy import units as u +from astropy.io import fits from gwcs import WCS from gwcs import coordinate_frames as cf From 55f0a316a219e823605764aaea6d00173eafca88 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:36:56 -0400 Subject: [PATCH 093/117] Added import for highlevel WCS --- tests/test_alignment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 4b222ab8..5a7dcaf9 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -5,6 +5,7 @@ from astropy import units as u from astropy.io import fits +from astropy.wcs import WCS as highlevelWCS from gwcs import WCS from gwcs import coordinate_frames as cf @@ -232,7 +233,7 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): assert type(exec_info.value) == expected_error def get_fake_wcs(): - fake_wcs1 = WCS( + fake_wcs1 = highlevelWCS( fits.Header( { "NAXIS": 2, @@ -249,7 +250,7 @@ def get_fake_wcs(): } ) ) - fake_wcs2 = WCS( + fake_wcs2 = highlevelWCS( fits.Header( { "NAXIS": 2, From 0f294091ce3831e834350428a7ed8d8f8fae9890 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:45:47 -0400 Subject: [PATCH 094/117] Small text change. --- tests/test_alignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 5a7dcaf9..919cca8c 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -278,7 +278,7 @@ def get_fake_wcs(): pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test ], ) -def test__reproject(x_inp, y_inp, x_expected, y_expected): +def test_reproject(x_inp, y_inp, x_expected, y_expected): wcs1, wcs2 = get_fake_wcs() f = reproject.reproject_coords(wcs1, wcs2) x_out, y_out = f(x_inp, y_inp) From 161d9fd253b7ebabc590795bc73a04e9bb788f32 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:06:15 -0400 Subject: [PATCH 095/117] Files moved, added calculate_pixmap --- src/stcal/alignment/reproject.py | 89 ---------------------- src/stcal/alignment/resample_utils.py | 22 ++++++ src/stcal/alignment/util.py | 103 +++++++++++++++++++++++++- tests/test_alignment.py | 8 +- 4 files changed, 131 insertions(+), 91 deletions(-) delete mode 100644 src/stcal/alignment/reproject.py diff --git a/src/stcal/alignment/reproject.py b/src/stcal/alignment/reproject.py deleted file mode 100644 index 3c1af6f9..00000000 --- a/src/stcal/alignment/reproject.py +++ /dev/null @@ -1,89 +0,0 @@ -import gwcs -import numpy as np -from astropy import wcs as fitswcs -from astropy.modeling import Model -from typing import Union - - -def reproject_coords(wcs1, wcs2): - """ - Given two WCSs or transforms return a function which takes pixel - coordinates in the first WCS or transform and computes them in pixel coordinates - in the second one. It performs the forward transformation of ``wcs1`` followed by the - inverse of ``wcs2``. - - Parameters - ---------- - wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` - WCS objects. - - Returns - ------- - _reproject : func - Function to compute the transformations. It takes x, y - positions in ``wcs1`` and returns x, y positions in ``wcs2``. - """ - - def _get_forward_transform_func(wcs1): - """Get the forward transform function from the input WCS. If the wcs is a - fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), - y (str, ndarray), and origin (int). The origin should be between 0, and 1 - https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file - ) - """ # noqa : E501 - if isinstance(wcs1, fitswcs.WCS): - forward_transform = wcs1.all_pix2world - elif isinstance(wcs1, gwcs.WCS): - forward_transform = wcs1.forward_transform - elif issubclass(wcs1, Model): - forward_transform = wcs1 - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return forward_transform - - def _get_backward_transform_func(wcs2): - if isinstance(wcs2, fitswcs.WCS): - backward_transform = wcs2.all_world2pix - elif isinstance(wcs2, gwcs.WCS): - backward_transform = wcs2.backward_transform - elif issubclass(wcs2, Model): - backward_transform = wcs2.inverse - else: - raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" - ) - return backward_transform - - def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: - """ - Reprojects the input coordinates from one WCS to another. - - Parameters: - ----------- - x : str or np.ndarray - Array of x-coordinates to be reprojected. - y : str or np.ndarray - Array of y-coordinates to be reprojected. - - Returns: - -------- - tuple - Tuple of reprojected x and y coordinates. - """ - # example inputs to resulting function (12, 13, 0) # third number is origin - if not isinstance(x, list): - x = [x] - if not isinstance(y, list): - y = [y] - if len(x) != len(y): - raise ValueError("x and y must be the same length") - sky = _get_forward_transform_func(wcs1)(x, y, 0) - sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) - new_sky = tuple(sky_back[:, :1].flatten()) - return tuple(new_sky) - - return _reproject diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index e69de29b..0f44384e 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -0,0 +1,22 @@ +import gwcs +import logging +import numpy as np +from util import wcs_bbox_from_shape, reproject + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + +def calc_pixmap(in_wcs, out_wcs, shape=None): + """ Return a pixel grid map from input frame to output frame. + """ + if shape: + bb = wcs_bbox_from_shape(shape) + log.debug("Bounding box from data shape: {}".format(bb)) + else: + bb = in_wcs.bounding_box + log.debug("Bounding box from WCS: {}".format(in_wcs.bounding_box)) + + grid = gwcs.wcstools.grid_from_bounding_box(bb) + pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) + + return pixmap \ No newline at end of file diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 1346f683..1fc9f319 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -15,7 +15,8 @@ from asdf import AsdfFile from gwcs import WCS -from gwcs.wcstools import wcs_from_fiducial +from astropy import wcs as fitswcs +from gwcs.wcstools import wcs_from_fiducial, grid_from_bounding_box log = logging.getLogger(__name__) @@ -27,6 +28,7 @@ "compute_fiducial", "calc_rotation_matrix", "wcs_from_footprints", + 'reproject', ] @@ -787,3 +789,102 @@ def update_s_region_keyword(model, footprint): else: model.meta.wcsinfo.s_region = s_region log.info(f"Update S_REGION to {model.meta.wcsinfo.s_region}") + + +def reproject(wcs1, wcs2): + """ + Given two WCSs or transforms return a function which takes pixel + coordinates in the first WCS or transform and computes them in pixel coordinates + in the second one. It performs the forward transformation of ``wcs1`` followed by the + inverse of ``wcs2``. + + Parameters + ---------- + wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + WCS objects. + + Returns + ------- + _reproject : func + Function to compute the transformations. It takes x, y + positions in ``wcs1`` and returns x, y positions in ``wcs2``. + """ + + def _get_forward_transform_func(wcs1): + """Get the forward transform function from the input WCS. If the wcs is a + fitswcs.WCS object all_pix2world requres three inputs, the x (str, ndarrray), + y (str, ndarray), and origin (int). The origin should be between 0, and 1 + https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file + ) + """ # noqa : E501 + if isinstance(wcs1, fitswcs.WCS): + forward_transform = wcs1.all_pix2world + elif isinstance(wcs1, WCS): + forward_transform = wcs1.forward_transform + elif issubclass(wcs1, Model): + forward_transform = wcs1 + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return forward_transform + + def _get_backward_transform_func(wcs2): + if isinstance(wcs2, fitswcs.WCS): + backward_transform = wcs2.all_world2pix + elif isinstance(wcs2, WCS): + backward_transform = wcs2.backward_transform + elif issubclass(wcs2, Model): + backward_transform = wcs2.inverse + else: + raise TypeError( + "Expected input to be astropy.wcs.WCS or gwcs.WCS " + "object or astropy.modeling.Model subclass" + ) + return backward_transform + + def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: + """ + Reprojects the input coordinates from one WCS to another. + + Parameters: + ----------- + x : str or np.ndarray + Array of x-coordinates to be reprojected. + y : str or np.ndarray + Array of y-coordinates to be reprojected. + + Returns: + -------- + tuple + Tuple of reprojected x and y coordinates. + """ + # example inputs to resulting function (12, 13, 0) # third number is origin + if not isinstance(x, list): + x = [x] + if not isinstance(y, list): + y = [y] + if len(x) != len(y): + raise ValueError("x and y must be the same length") + sky = _get_forward_transform_func(wcs1)(x, y, 0) + sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) + new_sky = tuple(sky_back[:, :1].flatten()) + return tuple(new_sky) + + return _reproject + +def calc_gwcs_pixmap(in_wcs, out_wcs, shape=None): + """ Return a pixel grid map from input frame to output frame. + """ + if shape: + bb = wcs_bbox_from_shape(shape) + log.debug("Bounding box from data shape: {}".format(bb)) + else: + bb = in_wcs.bounding_box + log.debug("Bounding box from WCS: {}".format(in_wcs.bounding_box)) + + grid = grid_from_bounding_box(bb) + pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) + + return pixmap \ No newline at end of file diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 919cca8c..9b97c219 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -8,6 +8,7 @@ from astropy.wcs import WCS as highlevelWCS from gwcs import WCS from gwcs import coordinate_frames as cf +from stdatamodels.jwst import datamodels import pytest from stcal.alignment import reproject @@ -19,6 +20,7 @@ update_s_region_keyword, wcs_bbox_from_shape, update_s_region_imaging, + reproject, ) @@ -280,10 +282,14 @@ def get_fake_wcs(): ) def test_reproject(x_inp, y_inp, x_expected, y_expected): wcs1, wcs2 = get_fake_wcs() - f = reproject.reproject_coords(wcs1, wcs2) + f = reproject(wcs1, wcs2) x_out, y_out = f(x_inp, y_inp) assert np.allclose(x_out, x_expected, rtol=1e-05) +def test_wcs_bbox_from_shape_2d(): + bb = wcs_bbox_from_shape((512, 2048)) + assert bb == ((-0.5, 2047.5), (-0.5, 511.5)) + @pytest.mark.parametrize( "model, footprint, expected_s_region, expected_log_info", [ From d426201a7532d03d5ce1a755a35102ebe11301df Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:22:27 -0400 Subject: [PATCH 096/117] Ken's suggestions. --- src/stcal/alignment/resample_utils.py | 19 +++++++-- src/stcal/alignment/util.py | 61 ++++++--------------------- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 0f44384e..3a412e45 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -7,8 +7,22 @@ log.setLevel(logging.DEBUG) def calc_pixmap(in_wcs, out_wcs, shape=None): - """ Return a pixel grid map from input frame to output frame. - """ + """Return a pixel grid map from input frame to output frame + + Parameters + ---------- + in_wcs: `~astropy.wcs.WCS` + Input WCS objects or transforms. + in_wcs: `~astropy.wcs.WCS` + output WCS objects or transforms. + shape : tuple, optional + Shape of grid in pixels. The default is None. + + Returns + ------- + pixmap + reprojected pixel grid map + """ if shape: bb = wcs_bbox_from_shape(shape) log.debug("Bounding box from data shape: {}".format(bb)) @@ -18,5 +32,4 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): grid = gwcs.wcstools.grid_from_bounding_box(bb) pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) - return pixmap \ No newline at end of file diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 1fc9f319..e49f8f3d 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -28,7 +28,7 @@ "compute_fiducial", "calc_rotation_matrix", "wcs_from_footprints", - 'reproject', + "reproject", ] @@ -66,9 +66,7 @@ def _calculate_fiducial_from_spatial_footprint( y_mid = (np.max(y) + np.min(y)) / 2.0 z_mid = (np.max(z) + np.min(z)) / 2.0 lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 - lat_fiducial = np.rad2deg( - np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) - ) + lat_fiducial = np.rad2deg(np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2))) return lon_fiducial, lat_fiducial @@ -134,9 +132,7 @@ def _generate_tranform( calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) ) - rotation = astmodels.AffineTransformation2D( - pc, name="pc_rotation_matrix" - ) + rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") transform = [rotation] if sky_axes: if not pscale: @@ -179,9 +175,7 @@ def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): ((x0_lower, x0_upper), (x1_lower, x1_upper)). """ footprints = [w.footprint().T for w in wcs_list] - domain_bounds = np.hstack( - [ref_wcs.backward_transform(*f) for f in footprints] - ) + domain_bounds = np.hstack([ref_wcs.backward_transform(*f) for f in footprints]) axis_min_values = np.min(domain_bounds, axis=1) domain_bounds = (domain_bounds.T - axis_min_values).T @@ -339,9 +333,7 @@ def _calculate_new_wcs( wcs_new.bounding_box = output_bounding_box if shape is None: - shape = [ - int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] - ] + shape = [int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1]] wcs_new.pixel_shape = shape[::-1] wcs_new.array_shape = shape @@ -371,9 +363,7 @@ def _validate_wcs_list(wcs_list): instance of WCS. """ if not isiterable(wcs_list): - raise ValueError( - "Expected 'wcs_list' to be an iterable of WCS objects." - ) + raise ValueError("Expected 'wcs_list' to be an iterable of WCS objects.") elif len(wcs_list): if not all(isinstance(w, WCS) for w in wcs_list): raise TypeError( @@ -470,9 +460,7 @@ def compute_scale( spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] delta[spatial_idx[0]] = 1 - crpix_with_offsets = np.vstack( - (crpix, crpix + delta, crpix + np.roll(delta, 1)) - ).T + crpix_with_offsets = np.vstack((crpix, crpix + delta, crpix + np.roll(delta, 1))).T crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) coords = SkyCoord( @@ -528,9 +516,7 @@ def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: axes_types = wcslist[0].output_frame.axes_type spatial_axes = np.array(axes_types) == "SPATIAL" spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack( - [w.footprint(bounding_box=bounding_box).T for w in wcslist] - ) + footprints = np.hstack([w.footprint(bounding_box=bounding_box).T for w in wcslist]) spatial_footprint = footprints[spatial_axes] spectral_footprint = footprints[spectral_axes] @@ -729,9 +715,7 @@ def update_s_region_imaging(model, center=True): ### which means we are interested in each pixel's vertice, not its center. ### By using center=True, a difference of 0.5 pixel should be accounted for ### when comparing the world coordinates of the bounding box and the footprint. - footprint = model.meta.wcs.footprint( - bbox, center=center, axis_type="spatial" - ).T + footprint = model.meta.wcs.footprint(bbox, center=center, axis_type="spatial").T # take only imaging footprint footprint = footprint[:2, :] @@ -800,8 +784,10 @@ def reproject(wcs1, wcs2): Parameters ---------- - wcs1, wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` - WCS objects. + wcs1: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + Input WCS objects or transforms. + wcs2: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + output WCS objects or transforms. Returns ------- @@ -816,13 +802,11 @@ def _get_forward_transform_func(wcs1): y (str, ndarray), and origin (int). The origin should be between 0, and 1 https://docs.astropy.org/en/latest/wcs/index.html#loading-wcs-information-from-a-fits-file ) - """ # noqa : E501 + """ # noqa : E501 if isinstance(wcs1, fitswcs.WCS): forward_transform = wcs1.all_pix2world elif isinstance(wcs1, WCS): forward_transform = wcs1.forward_transform - elif issubclass(wcs1, Model): - forward_transform = wcs1 else: raise TypeError( "Expected input to be astropy.wcs.WCS or gwcs.WCS " @@ -835,8 +819,6 @@ def _get_backward_transform_func(wcs2): backward_transform = wcs2.all_world2pix elif isinstance(wcs2, WCS): backward_transform = wcs2.backward_transform - elif issubclass(wcs2, Model): - backward_transform = wcs2.inverse else: raise TypeError( "Expected input to be astropy.wcs.WCS or gwcs.WCS " @@ -873,18 +855,3 @@ def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: return tuple(new_sky) return _reproject - -def calc_gwcs_pixmap(in_wcs, out_wcs, shape=None): - """ Return a pixel grid map from input frame to output frame. - """ - if shape: - bb = wcs_bbox_from_shape(shape) - log.debug("Bounding box from data shape: {}".format(bb)) - else: - bb = in_wcs.bounding_box - log.debug("Bounding box from WCS: {}".format(in_wcs.bounding_box)) - - grid = grid_from_bounding_box(bb) - pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) - - return pixmap \ No newline at end of file From 23f2003fa887ecd9167ea3ac03b783c92415812f Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:36:16 -0400 Subject: [PATCH 097/117] consistently named imports --- src/stcal/alignment/resample_utils.py | 4 ++-- src/stcal/alignment/util.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 3a412e45..3c663674 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -1,7 +1,7 @@ -import gwcs import logging import numpy as np from util import wcs_bbox_from_shape, reproject +from gwcs.wcstools import grid_from_bounding_box log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -30,6 +30,6 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): bb = in_wcs.bounding_box log.debug("Bounding box from WCS: {}".format(in_wcs.bounding_box)) - grid = gwcs.wcstools.grid_from_bounding_box(bb) + grid = grid_from_bounding_box(bb) pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) return pixmap \ No newline at end of file diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index e49f8f3d..bf668489 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -12,11 +12,11 @@ from astropy.utils.misc import isiterable from astropy import units as u from astropy.modeling import models as astmodels +from astropy import wcs as fitswcs from asdf import AsdfFile from gwcs import WCS -from astropy import wcs as fitswcs -from gwcs.wcstools import wcs_from_fiducial, grid_from_bounding_box +from gwcs.wcstools import wcs_from_fiducial log = logging.getLogger(__name__) From 15c3ecd62b73c5a87ad512efb7326c22e67daa31 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:39:16 -0400 Subject: [PATCH 098/117] formatting --- src/stcal/alignment/resample_utils.py | 7 ++++--- tests/test_alignment.py | 27 +++++++++++---------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 3c663674..f4b89916 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -6,12 +6,13 @@ log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) + def calc_pixmap(in_wcs, out_wcs, shape=None): """Return a pixel grid map from input frame to output frame Parameters ---------- - in_wcs: `~astropy.wcs.WCS` + in_wcs: `~astropy.wcs.WCS` Input WCS objects or transforms. in_wcs: `~astropy.wcs.WCS` output WCS objects or transforms. @@ -22,7 +23,7 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): ------- pixmap reprojected pixel grid map - """ + """ if shape: bb = wcs_bbox_from_shape(shape) log.debug("Bounding box from data shape: {}".format(bb)) @@ -32,4 +33,4 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): grid = grid_from_bounding_box(bb) pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) - return pixmap \ No newline at end of file + return pixmap diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 9b97c219..aef2ef3f 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -5,10 +5,9 @@ from astropy import units as u from astropy.io import fits -from astropy.wcs import WCS as highlevelWCS +from astropy import wcs as fitswcs from gwcs import WCS from gwcs import coordinate_frames as cf -from stdatamodels.jwst import datamodels import pytest from stcal.alignment import reproject @@ -100,20 +99,14 @@ def __init__(self): class MetaData: - def __init__( - self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None - ): - self.wcsinfo = WcsInfo( - ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle - ) + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): + self.wcsinfo = WcsInfo(ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle) self.wcs = wcs self.coordinates = Coordinates() class DataModel: - def __init__( - self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None - ): + def __init__(self, ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=None): self.meta = MetaData( ra_ref, dec_ref, roll_ref, v2_ref, v3_ref, v3yangle, wcs=wcs ) @@ -137,9 +130,7 @@ def test_compute_fiducial(): assert all(np.isclose(wcs(1, 1), computed_fiducial)) -@pytest.mark.parametrize( - "pscales", [(0.000014, 0.000014), (0.000028, 0.000014)] -) +@pytest.mark.parametrize("pscales", [(0.000014, 0.000014), (0.000028, 0.000014)]) def test_compute_scale(pscales): """Test that util.compute_scale can properly determine the pixel scale of a WCS object. @@ -234,8 +225,9 @@ def test_validate_wcs_list_invalid(wcs_list, expected_error): assert type(exec_info.value) == expected_error + def get_fake_wcs(): - fake_wcs1 = highlevelWCS( + fake_wcs1 = fitswcs.WCS( fits.Header( { "NAXIS": 2, @@ -252,7 +244,7 @@ def get_fake_wcs(): } ) ) - fake_wcs2 = highlevelWCS( + fake_wcs2 = fitswcs.WCS( fits.Header( { "NAXIS": 2, @@ -285,11 +277,14 @@ def test_reproject(x_inp, y_inp, x_expected, y_expected): f = reproject(wcs1, wcs2) x_out, y_out = f(x_inp, y_inp) assert np.allclose(x_out, x_expected, rtol=1e-05) + assert np.allclose(y_out, y_expected, rtol=1e-05) + def test_wcs_bbox_from_shape_2d(): bb = wcs_bbox_from_shape((512, 2048)) assert bb == ((-0.5, 2047.5), (-0.5, 511.5)) + @pytest.mark.parametrize( "model, footprint, expected_s_region, expected_log_info", [ From d6818d6b84f4f8cf1ba4b30a548dd03bc91b9c62 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:39:25 -0400 Subject: [PATCH 099/117] formatting --- src/stcal/alignment/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index bf668489..4203da9c 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -784,9 +784,9 @@ def reproject(wcs1, wcs2): Parameters ---------- - wcs1: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + wcs1: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` Input WCS objects or transforms. - wcs2: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` or `~astropy.modeling.Model` + wcs2: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` output WCS objects or transforms. Returns @@ -810,7 +810,7 @@ def _get_forward_transform_func(wcs1): else: raise TypeError( "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" + "object" ) return forward_transform @@ -822,7 +822,7 @@ def _get_backward_transform_func(wcs2): else: raise TypeError( "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object or astropy.modeling.Model subclass" + "object" ) return backward_transform From 87d732bb63f869b8e5254c4c05195a0e942c3857 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:59:29 -0400 Subject: [PATCH 100/117] using pixel shape instead of bounding box --- src/stcal/alignment/resample_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index f4b89916..ecaf8ac3 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -28,8 +28,8 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): bb = wcs_bbox_from_shape(shape) log.debug("Bounding box from data shape: {}".format(bb)) else: - bb = in_wcs.bounding_box - log.debug("Bounding box from WCS: {}".format(in_wcs.bounding_box)) + bb = in_wcs.pixel_shape + log.debug("Bounding box from WCS: {}".format(bb)) grid = grid_from_bounding_box(bb) pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) From b8a3b9c25b01d5e9f48d429c47a5303dacfcf64c Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:55:22 -0400 Subject: [PATCH 101/117] Update src/stcal/alignment/resample_utils.py Co-authored-by: Nadia Dencheva --- src/stcal/alignment/resample_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index ecaf8ac3..869ac4b4 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -14,7 +14,7 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): ---------- in_wcs: `~astropy.wcs.WCS` Input WCS objects or transforms. - in_wcs: `~astropy.wcs.WCS` + out_wcs: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` output WCS objects or transforms. shape : tuple, optional Shape of grid in pixels. The default is None. From 6c5242e35406fc7309d1ab7454fb934beffeb883 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:55:35 -0400 Subject: [PATCH 102/117] Update src/stcal/alignment/util.py Co-authored-by: Nadia Dencheva --- src/stcal/alignment/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4203da9c..7290109a 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -787,7 +787,7 @@ def reproject(wcs1, wcs2): wcs1: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` Input WCS objects or transforms. wcs2: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` - output WCS objects or transforms. + Output WCS objects or transforms. Returns ------- From bed60fa2376399f311accd7db970ffdc8289fd1d Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:04:24 -0400 Subject: [PATCH 103/117] Added correct shape, gwcs.WCS instead of WCS --- src/stcal/alignment/resample_utils.py | 2 +- src/stcal/alignment/util.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 869ac4b4..e68598cc 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -28,7 +28,7 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): bb = wcs_bbox_from_shape(shape) log.debug("Bounding box from data shape: {}".format(bb)) else: - bb = in_wcs.pixel_shape + bb = wcs_bbox_from_shape(in_wcs.pixel_shape) log.debug("Bounding box from WCS: {}".format(bb)) grid = grid_from_bounding_box(bb) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 7290109a..fba78af4 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -15,7 +15,7 @@ from astropy import wcs as fitswcs from asdf import AsdfFile -from gwcs import WCS +import gwcs from gwcs.wcstools import wcs_from_fiducial @@ -365,7 +365,7 @@ def _validate_wcs_list(wcs_list): if not isiterable(wcs_list): raise ValueError("Expected 'wcs_list' to be an iterable of WCS objects.") elif len(wcs_list): - if not all(isinstance(w, WCS) for w in wcs_list): + if not all(isinstance(w, gwcs.WCS) for w in wcs_list): raise TypeError( "All items in 'wcs_list' are to be instances of gwcs.wcs.WCS." ) @@ -420,7 +420,7 @@ def wcsinfo_from_model(input_model: SupportsDataWithWcs): def compute_scale( - wcs: WCS, + wcs: gwcs.WCS, fiducial: Union[tuple, np.ndarray], disp_axis: int = None, pscale_ratio: float = None, @@ -805,7 +805,7 @@ def _get_forward_transform_func(wcs1): """ # noqa : E501 if isinstance(wcs1, fitswcs.WCS): forward_transform = wcs1.all_pix2world - elif isinstance(wcs1, WCS): + elif isinstance(wcs1, gwcs.WCS): forward_transform = wcs1.forward_transform else: raise TypeError( @@ -817,7 +817,7 @@ def _get_forward_transform_func(wcs1): def _get_backward_transform_func(wcs2): if isinstance(wcs2, fitswcs.WCS): backward_transform = wcs2.all_world2pix - elif isinstance(wcs2, WCS): + elif isinstance(wcs2, gwcs.WCS): backward_transform = wcs2.backward_transform else: raise TypeError( From 9753c59034be273a4c999612dc88934fc8909b69 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:51:42 -0400 Subject: [PATCH 104/117] added test_calc_pixmap --- tests/test_alignment.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index aef2ef3f..16f84528 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -6,11 +6,12 @@ from astropy.io import fits from astropy import wcs as fitswcs -from gwcs import WCS +import gwcs from gwcs import coordinate_frames as cf import pytest from stcal.alignment import reproject +from stcal.alignment import resample_utils from stcal.alignment.util import ( compute_fiducial, compute_scale, @@ -52,7 +53,7 @@ def _create_wcs_object_without_distortion( pipeline = [(detector_frame, det2sky), (sky_frame, None)] - wcs_obj = WCS(pipeline) + wcs_obj = gwcs.WCS(pipeline) wcs_obj.bounding_box = ( (-0.5, shape[-1] - 0.5), @@ -285,6 +286,20 @@ def test_wcs_bbox_from_shape_2d(): assert bb == ((-0.5, 2047.5), (-0.5, 511.5)) +@pytest.mark.parametrize( + "shape, pixmap_expected_shape", + [ + (None,(1,1,32)), + ((100, 200), (100,200)), + pytest.param((1), marks=pytest.mark.xfail), # expected failure test + ], +) +def test_calc_pixmap(shape, pixmap_expected_shape): + wcs1, wcs2 = get_fake_wcs() + pixmap = resample_utils.calc_pixmap(wcs1, wcs2, shape=shape) + assert pixmap.shape==pixmap_expected_shape + + @pytest.mark.parametrize( "model, footprint, expected_s_region, expected_log_info", [ From ab413c073c914684943e6526638b2590c96c5a60 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:55:49 -0400 Subject: [PATCH 105/117] relative import of util --- src/stcal/alignment/resample_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index e68598cc..53a6aa64 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -1,6 +1,6 @@ import logging import numpy as np -from util import wcs_bbox_from_shape, reproject +from . import util from gwcs.wcstools import grid_from_bounding_box log = logging.getLogger(__name__) @@ -25,12 +25,12 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): reprojected pixel grid map """ if shape: - bb = wcs_bbox_from_shape(shape) + bb = util.wcs_bbox_from_shape(shape) log.debug("Bounding box from data shape: {}".format(bb)) else: - bb = wcs_bbox_from_shape(in_wcs.pixel_shape) + bb = util.wcs_bbox_from_shape(in_wcs.pixel_shape) log.debug("Bounding box from WCS: {}".format(bb)) grid = grid_from_bounding_box(bb) - pixmap = np.dstack(reproject(in_wcs, out_wcs)(grid[0], grid[1])) + pixmap = np.dstack(util.reproject(in_wcs, out_wcs)(grid[0], grid[1])) return pixmap From 6281ba29d3430bac8c907a203a34ea723bba4524 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:02:11 -0400 Subject: [PATCH 106/117] tests passing --- tests/test_alignment.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 16f84528..6168e232 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -232,8 +232,8 @@ def get_fake_wcs(): fits.Header( { "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, + "NAXIS1": 4, + "NAXIS2": 4, "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CRVAL1": 0, @@ -249,8 +249,8 @@ def get_fake_wcs(): fits.Header( { "NAXIS": 2, - "NAXIS1": 1, - "NAXIS2": 1, + "NAXIS1": 5, + "NAXIS2": 5, "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CRVAL1": 0, @@ -290,8 +290,7 @@ def test_wcs_bbox_from_shape_2d(): "shape, pixmap_expected_shape", [ (None,(1,1,32)), - ((100, 200), (100,200)), - pytest.param((1), marks=pytest.mark.xfail), # expected failure test + ((100, 200), (1, 1, 40000)), ], ) def test_calc_pixmap(shape, pixmap_expected_shape): From dc0e521546c20b3d6b73536b14d978fca36878c9 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 5 Sep 2023 09:55:51 -0400 Subject: [PATCH 107/117] removed doubly imported reproject --- tests/test_alignment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 6168e232..63f87423 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -10,7 +10,6 @@ from gwcs import coordinate_frames as cf import pytest -from stcal.alignment import reproject from stcal.alignment import resample_utils from stcal.alignment.util import ( compute_fiducial, From d6df8d42ac295a572d521c57042a5c4b1d1de239 Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:34:54 -0400 Subject: [PATCH 108/117] output shape corrected --- src/stcal/alignment/resample_utils.py | 7 +++-- src/stcal/alignment/util.py | 37 ++++++++++++++++----------- tests/test_alignment.py | 8 +++--- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 53a6aa64..b271aaa4 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -1,6 +1,6 @@ import logging import numpy as np -from . import util +from stcal.alignment import util from gwcs.wcstools import grid_from_bounding_box log = logging.getLogger(__name__) @@ -31,6 +31,9 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): bb = util.wcs_bbox_from_shape(in_wcs.pixel_shape) log.debug("Bounding box from WCS: {}".format(bb)) + # creates 2 grids, one with rows of all x values * len(y) rows, + # and the reverse for all y columns grid = grid_from_bounding_box(bb) - pixmap = np.dstack(util.reproject(in_wcs, out_wcs)(grid[0], grid[1])) + transform_function = util.reproject(in_wcs, out_wcs) + pixmap = np.dstack(transform_function(grid[0], grid[1])) return pixmap diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index fba78af4..611f96ba 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -826,32 +826,39 @@ def _get_backward_transform_func(wcs2): ) return backward_transform - def _reproject(x: Union[str, np.ndarray], y: Union[str, np.ndarray]) -> tuple: + def _reproject(x: Union[float, np.ndarray], y: Union[float, np.ndarray]) -> tuple: """ Reprojects the input coordinates from one WCS to another. Parameters: ----------- - x : str or np.ndarray - Array of x-coordinates to be reprojected. - y : str or np.ndarray - Array of y-coordinates to be reprojected. + x : float or np.ndarray + x-coordinate(s) to be reprojected. + y : float or np.ndarray + y-coordinate(s) to be reprojected. Returns: -------- tuple - Tuple of reprojected x and y coordinates. + Tuple of np.ndarrays including reprojected x and y coordinates. """ # example inputs to resulting function (12, 13, 0) # third number is origin - if not isinstance(x, list): - x = [x] - if not isinstance(y, list): - y = [y] - if len(x) != len(y): + # uses np.arrays for shape functionality + if not isinstance(x, (np.ndarray)): + x = np.array(x) + if not isinstance(y, (np.ndarray)): + y = np.array(y) + if x.shape != y.shape: raise ValueError("x and y must be the same length") sky = _get_forward_transform_func(wcs1)(x, y, 0) - sky_back = np.array(_get_backward_transform_func(wcs2)(sky[0], sky[1], 0)) - new_sky = tuple(sky_back[:, :1].flatten()) - return tuple(new_sky) - + + # rearrange into array including flattened x and y vaues + flat_sky = [] + for axis in sky: + flat_sky.append(axis.flatten()) + det = np.array(_get_backward_transform_func(wcs2)(flat_sky[0], flat_sky[1], 0)) + det_reshaped = [] + for axis in det: + det_reshaped.append(axis.reshape(x.shape)) + return tuple(det_reshaped) return _reproject diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 63f87423..52193b36 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -267,8 +267,8 @@ def get_fake_wcs(): @pytest.mark.parametrize( "x_inp, y_inp, x_expected, y_expected", [ - (1000, 2000, 2000, 4000), # string input test - ([1000], [2000], 2000, 4000), # array input test + (1000, 2000, np.array(2000), np.array(4000)), # string input test + ([1000], [2000], np.array(2000), np.array(4000)), # array input test pytest.param(1, 2, 3, 4, marks=pytest.mark.xfail), # expected failure test ], ) @@ -288,8 +288,8 @@ def test_wcs_bbox_from_shape_2d(): @pytest.mark.parametrize( "shape, pixmap_expected_shape", [ - (None,(1,1,32)), - ((100, 200), (1, 1, 40000)), + (None,(4,4,2)), + ((100, 200), (100, 200, 2)), ], ) def test_calc_pixmap(shape, pixmap_expected_shape): From 6502ad7b984efd7c26abde03acb384e88bb912fc Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:36:46 -0400 Subject: [PATCH 109/117] Removed whitespace --- src/stcal/alignment/resample_utils.py | 4 ++-- src/stcal/alignment/util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index b271aaa4..2ac37182 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -31,8 +31,8 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): bb = util.wcs_bbox_from_shape(in_wcs.pixel_shape) log.debug("Bounding box from WCS: {}".format(bb)) - # creates 2 grids, one with rows of all x values * len(y) rows, - # and the reverse for all y columns + # creates 2 grids, one with rows of all x values * len(y) rows, + # and the reverse for all y columns grid = grid_from_bounding_box(bb) transform_function = util.reproject(in_wcs, out_wcs) pixmap = np.dstack(transform_function(grid[0], grid[1])) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 611f96ba..228029e6 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -851,7 +851,7 @@ def _reproject(x: Union[float, np.ndarray], y: Union[float, np.ndarray]) -> tupl if x.shape != y.shape: raise ValueError("x and y must be the same length") sky = _get_forward_transform_func(wcs1)(x, y, 0) - + # rearrange into array including flattened x and y vaues flat_sky = [] for axis in sky: From 0381ab257fd67bde7baa5bd42babec09aa2fd6be Mon Sep 17 00:00:00 2001 From: Steve Goldman <32876747+s-goldman@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:32:30 -0400 Subject: [PATCH 110/117] Added Nadia's code suggestions --- src/stcal/alignment/resample_utils.py | 9 +++++---- src/stcal/alignment/util.py | 6 +++--- tests/test_alignment.py | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/stcal/alignment/resample_utils.py b/src/stcal/alignment/resample_utils.py index 2ac37182..7a0c04d3 100644 --- a/src/stcal/alignment/resample_utils.py +++ b/src/stcal/alignment/resample_utils.py @@ -12,17 +12,18 @@ def calc_pixmap(in_wcs, out_wcs, shape=None): Parameters ---------- - in_wcs: `~astropy.wcs.WCS` + in_wcs : `~astropy.wcs.WCS` Input WCS objects or transforms. - out_wcs: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` + out_wcs : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` output WCS objects or transforms. shape : tuple, optional Shape of grid in pixels. The default is None. Returns ------- - pixmap - reprojected pixel grid map + pixmap : ndarray of shape (xdim, ydim, 2) + Reprojected pixel grid map. `pixmap[xin, yin]` returns `xout, + yout` indices in the output image. """ if shape: bb = util.wcs_bbox_from_shape(shape) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 228029e6..4042467f 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -425,7 +425,7 @@ def compute_scale( disp_axis: int = None, pscale_ratio: float = None, ) -> float: - """Compute scaling transform. + """Compute the scale at the fiducial point on the detector.. Parameters ---------- @@ -784,9 +784,9 @@ def reproject(wcs1, wcs2): Parameters ---------- - wcs1: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` + wcs1 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` Input WCS objects or transforms. - wcs2: `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` + wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` Output WCS objects or transforms. Returns diff --git a/tests/test_alignment.py b/tests/test_alignment.py index 52193b36..80af4d7e 100644 --- a/tests/test_alignment.py +++ b/tests/test_alignment.py @@ -288,11 +288,12 @@ def test_wcs_bbox_from_shape_2d(): @pytest.mark.parametrize( "shape, pixmap_expected_shape", [ - (None,(4,4,2)), + (None,(4, 4, 2)), ((100, 200), (100, 200, 2)), ], ) -def test_calc_pixmap(shape, pixmap_expected_shape): +def test_calc_pixmap_shape(shape, pixmap_expected_shape): + # TODO: add test for gwcs.WCS wcs1, wcs2 = get_fake_wcs() pixmap = resample_utils.calc_pixmap(wcs1, wcs2, shape=shape) assert pixmap.shape==pixmap_expected_shape From e7801ddb0f350e679deda6aa4d428c1d78c84c0f Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 28 Sep 2023 10:01:09 -0400 Subject: [PATCH 111/117] Fix docstring returned type issue. --- src/stcal/alignment/util.py | 48 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 4042467f..71f7d87a 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -66,7 +66,9 @@ def _calculate_fiducial_from_spatial_footprint( y_mid = (np.max(y) + np.min(y)) / 2.0 z_mid = (np.max(z) + np.min(z)) / 2.0 lon_fiducial = np.rad2deg(np.arctan2(y_mid, x_mid)) % 360.0 - lat_fiducial = np.rad2deg(np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2))) + lat_fiducial = np.rad2deg( + np.arctan2(z_mid, np.sqrt(x_mid**2 + y_mid**2)) + ) return lon_fiducial, lat_fiducial @@ -132,7 +134,9 @@ def _generate_tranform( calc_rotation_matrix(roll_ref, v3yangle, vparity=vparity), (2, 2) ) - rotation = astmodels.AffineTransformation2D(pc, name="pc_rotation_matrix") + rotation = astmodels.AffineTransformation2D( + pc, name="pc_rotation_matrix" + ) transform = [rotation] if sky_axes: if not pscale: @@ -175,7 +179,9 @@ def _get_axis_min_and_bounding_box(ref_model, wcs_list, ref_wcs): ((x0_lower, x0_upper), (x1_lower, x1_upper)). """ footprints = [w.footprint().T for w in wcs_list] - domain_bounds = np.hstack([ref_wcs.backward_transform(*f) for f in footprints]) + domain_bounds = np.hstack( + [ref_wcs.backward_transform(*f) for f in footprints] + ) axis_min_values = np.min(domain_bounds, axis=1) domain_bounds = (domain_bounds.T - axis_min_values).T @@ -333,7 +339,9 @@ def _calculate_new_wcs( wcs_new.bounding_box = output_bounding_box if shape is None: - shape = [int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1]] + shape = [ + int(axs[1] - axs[0] + 0.5) for axs in output_bounding_box[::-1] + ] wcs_new.pixel_shape = shape[::-1] wcs_new.array_shape = shape @@ -363,7 +371,9 @@ def _validate_wcs_list(wcs_list): instance of WCS. """ if not isiterable(wcs_list): - raise ValueError("Expected 'wcs_list' to be an iterable of WCS objects.") + raise ValueError( + "Expected 'wcs_list' to be an iterable of WCS objects." + ) elif len(wcs_list): if not all(isinstance(w, gwcs.WCS) for w in wcs_list): raise TypeError( @@ -460,7 +470,9 @@ def compute_scale( spatial_idx = np.where(np.array(wcs.output_frame.axes_type) == "SPATIAL")[0] delta[spatial_idx[0]] = 1 - crpix_with_offsets = np.vstack((crpix, crpix + delta, crpix + np.roll(delta, 1))).T + crpix_with_offsets = np.vstack( + (crpix, crpix + delta, crpix + np.roll(delta, 1)) + ).T crval_with_offsets = wcs(*crpix_with_offsets, with_bounding_box=False) coords = SkyCoord( @@ -516,7 +528,9 @@ def compute_fiducial(wcslist: list, bounding_box=None) -> np.ndarray: axes_types = wcslist[0].output_frame.axes_type spatial_axes = np.array(axes_types) == "SPATIAL" spectral_axes = np.array(axes_types) == "SPECTRAL" - footprints = np.hstack([w.footprint(bounding_box=bounding_box).T for w in wcslist]) + footprints = np.hstack( + [w.footprint(bounding_box=bounding_box).T for w in wcslist] + ) spatial_footprint = footprints[spatial_axes] spectral_footprint = footprints[spectral_axes] @@ -715,7 +729,9 @@ def update_s_region_imaging(model, center=True): ### which means we are interested in each pixel's vertice, not its center. ### By using center=True, a difference of 0.5 pixel should be accounted for ### when comparing the world coordinates of the bounding box and the footprint. - footprint = model.meta.wcs.footprint(bbox, center=center, axis_type="spatial").T + footprint = model.meta.wcs.footprint( + bbox, center=center, axis_type="spatial" + ).T # take only imaging footprint footprint = footprint[:2, :] @@ -791,7 +807,6 @@ def reproject(wcs1, wcs2): Returns ------- - _reproject : func Function to compute the transformations. It takes x, y positions in ``wcs1`` and returns x, y positions in ``wcs2``. """ @@ -809,8 +824,7 @@ def _get_forward_transform_func(wcs1): forward_transform = wcs1.forward_transform else: raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object" + "Expected input to be astropy.wcs.WCS or gwcs.WCS " "object" ) return forward_transform @@ -821,12 +835,13 @@ def _get_backward_transform_func(wcs2): backward_transform = wcs2.backward_transform else: raise TypeError( - "Expected input to be astropy.wcs.WCS or gwcs.WCS " - "object" + "Expected input to be astropy.wcs.WCS or gwcs.WCS " "object" ) return backward_transform - def _reproject(x: Union[float, np.ndarray], y: Union[float, np.ndarray]) -> tuple: + def _reproject( + x: Union[float, np.ndarray], y: Union[float, np.ndarray] + ) -> tuple: """ Reprojects the input coordinates from one WCS to another. @@ -856,9 +871,12 @@ def _reproject(x: Union[float, np.ndarray], y: Union[float, np.ndarray]) -> tupl flat_sky = [] for axis in sky: flat_sky.append(axis.flatten()) - det = np.array(_get_backward_transform_func(wcs2)(flat_sky[0], flat_sky[1], 0)) + det = np.array( + _get_backward_transform_func(wcs2)(flat_sky[0], flat_sky[1], 0) + ) det_reshaped = [] for axis in det: det_reshaped.append(axis.reshape(x.shape)) return tuple(det_reshaped) + return _reproject From 2626396b9ca3a3d989028a10131f365a19b68c5f Mon Sep 17 00:00:00 2001 From: Zach Burnett Date: Thu, 28 Sep 2023 11:48:34 -0400 Subject: [PATCH 112/117] use PyPI upload workflow from OpenAstronomy (#214) * use OpenAstronomy PyPI upload workflow * try copying Astropy's config (may need to omit arm entries) * force CPython * fix upload event filter --- .github/workflows/build.yml | 20 ++++++++++++++++++++ .github/workflows/publish-to-pypi.yml | 16 ---------------- pyproject.toml | 6 ++++++ 3 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/publish-to-pypi.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..9479373a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: build + +on: + release: + types: [ released ] + pull_request: + workflow_dispatch: + +jobs: + build: + uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@v1 + with: + upload_to_pypi: ${{ (github.event_name == 'release') && (github.event.action == 'released') }} + targets: | + - cp3?-manylinux_x86_64 + - cp3?-macosx_x86_64 + sdist: true + secrets: + pypi_token: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER }} + diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml deleted file mode 100644 index d36fd7d2..00000000 --- a/.github/workflows/publish-to-pypi.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Publish to PyPI - -on: - release: - types: [released] - -jobs: - publish: - uses: spacetelescope/action-publish_to_pypi/.github/workflows/workflow.yml@master - with: - test: false - build_platform_wheels: true # Set to true if your package contains a C extension - secrets: - user: ${{ secrets.PYPI_USERNAME_STSCI_MAINTAINER }} - password: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER }} # WARNING: Do not hardcode secret values here! If you want to use a different user or password, you can override this secret by creating one with the same name in your Github repository settings. - test_password: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER_TEST }} diff --git a/pyproject.toml b/pyproject.toml index 7b9855e3..d07ed759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,3 +93,9 @@ exclude = [ '.tox', '.eggs', ] + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] + +[tool.cibuildwheel.linux] +archs = ["auto", "aarch64"] \ No newline at end of file From af275b0443ad60a6b0397f2587add0dfa6d57fdd Mon Sep 17 00:00:00 2001 From: zacharyburnett Date: Thu, 28 Sep 2023 10:50:55 -0400 Subject: [PATCH 113/117] download WebbPSF data for downstream tests --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7e9e8f3..981ec3a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,13 +44,44 @@ jobs: python-version: '3.11' - linux: test-cov-xdist coverage: 'codecov' + data: + name: retrieve data + runs-on: ubuntu-latest + outputs: + data_path: ${{ steps.data.outputs.path }} + data_hash: ${{ steps.data_hash.outputs.hash }} + steps: + # webbpsf: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - id: data + run: | + echo "path=/tmp/data" >> $GITHUB_OUTPUT + echo "webbpsf_url=https://stsci.box.com/shared/static/n1fealx9q0m6sdnass6wnyfikvxtc0zz.gz" >> $GITHUB_OUTPUT + - run: | + mkdir -p tmp/data/ + mkdir -p ${{ steps.data.outputs.path }} + - run: wget ${{ steps.data.outputs.webbpsf_url }} -O tmp/minimal-webbpsf-data.tar.gz + - run: tar -xzvf tmp/minimal-webbpsf-data.tar.gz -C tmp/data/ + - id: data_hash + run: echo "hash=${{ hashFiles( 'tmp/data' ) }}" >> $GITHUB_OUTPUT + - run: mv tmp/data/* ${{ steps.data.outputs.path }} + - uses: actions/cache@v3 + with: + path: ${{ steps.data.outputs.path }} + key: data-${{ steps.data_hash.outputs.hash }} test_downstream: uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main + needs: [ data ] with: setenv: | CRDS_PATH: /tmp/crds_cache CRDS_CLIENT_RETRY_COUNT: 3 - CRDS_CLIENT_RETRY_DELAY_SECONDS: 20 + CRDS_CLIENT_RETRY_DELAY_SECONDS: 20 + WEBBPSF_PATH: ${{ needs.data.outputs.data_path }}/webbpsf-data + cache-path: ${{ needs.data.outputs.data_path }} + cache-key: data-${{ needs.data.outputs.data_hash }} envs: | - linux: test-jwst-cov-xdist - linux: test-romancal-cov-xdist From 5c4dff91413f24846dc46dca623cf191009b1c19 Mon Sep 17 00:00:00 2001 From: zacharyburnett Date: Thu, 28 Sep 2023 11:40:54 -0400 Subject: [PATCH 114/117] pass env var --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index ce419dcf..2484e81e 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,7 @@ deps = use_develop = true pass_env = CI + WEBBPSF_PATH set_env = devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple jwst: CRDS_SERVER_URL=https://jwst-crds.stsci.edu From bab0c14c2d6c56392116412cb400415059dab919 Mon Sep 17 00:00:00 2001 From: Zach Burnett Date: Mon, 2 Oct 2023 15:29:44 -0400 Subject: [PATCH 115/117] expand build matrix with more Python versions (#219) * copy build matrix from Astropy * parametrize the Python version --- .github/workflows/build.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9479373a..370c6172 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,12 @@ jobs: with: upload_to_pypi: ${{ (github.event_name == 'release') && (github.event.action == 'released') }} targets: | - - cp3?-manylinux_x86_64 - - cp3?-macosx_x86_64 + # Linux wheels + - cp3*-manylinux_x86_64 + # MacOS wheels + - cp3*-macosx_x86_64 + # Until we have arm64 runners, we can't automatically test arm64 wheels + - cp3*-macosx_arm64 sdist: true secrets: pypi_token: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER }} From e0cc4f2d4070c6b20e34112414d0d91285be7f85 Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Thu, 5 Oct 2023 14:58:25 -0400 Subject: [PATCH 116/117] Updates to docs and CI. --- .github/workflows/ci.yml | 2 -- docs/conf.py | 1 - src/stcal/alignment/util.py | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 981ec3a2..43cd6569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,11 @@ on: branches: - main - '*x' - - stcal-alignment tags: - '*' pull_request: branches: - main - - stcal-alignment schedule: # Weekly Monday 9AM build - cron: "0 9 * * 1" diff --git a/docs/conf.py b/docs/conf.py index 5ebcee52..fe87bb19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,6 @@ def setup(app): "matplotlib": ("https://matplotlib.org/stable", None), "gwcs": ("https://gwcs.readthedocs.io/en/latest/", None), "astropy": ("https://docs.astropy.org/en/stable/", None), - "stdatamodels": ("https://stdatamodels.readthedocs.io/en/latest/", None), } extensions = [ diff --git a/src/stcal/alignment/util.py b/src/stcal/alignment/util.py index 71f7d87a..762be702 100644 --- a/src/stcal/alignment/util.py +++ b/src/stcal/alignment/util.py @@ -800,9 +800,9 @@ def reproject(wcs1, wcs2): Parameters ---------- - wcs1 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` + wcs1 : astropy.wcs.WCS or gwcs.wcs.WCS Input WCS objects or transforms. - wcs2 : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` + wcs2 : astropy.wcs.WCS or gwcs.wcs.WCS Output WCS objects or transforms. Returns From 88d659e04bdca58083450229b670f48451ddf76c Mon Sep 17 00:00:00 2001 From: "M. Teodoro" Date: Fri, 6 Oct 2023 15:20:32 -0400 Subject: [PATCH 117/117] Move CHANGES.rst entry. --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5c397e78..66ba6c1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,8 @@ 1.4.5 (unreleased) ================== +- Added ``alignment`` sub-package. [#179] + Changes to API -------------- @@ -19,8 +21,6 @@ Other 1.4.4 (2023-09-15) ================== -- Added ``alignment`` sub-package. [#179] - Other -----