Repository: spacetelescope/jwst
Pull request #9307: JP-3858-wfs_combine
Author: login: penaguerrero
Labels: name: wfs_combine
name: no-changelog-entry-needed
Created: 2025-03-20T16:31:49Z | Merged: 2025-03-26T15:09:29Z

<!-- If this PR closes a JIRA ticket, make sure the title starts with the JIRA issue number,
for example JP-1234: <Fix a bug> -->
Resolves partially [JP-3858](https://jira.stsci.edu/browse/JP-3858)

<!-- If this PR closes a GitHub issue, reference it here by its number -->
Closes #

<!-- describe the changes comprising this PR here -->
This PR addresses style changes for was_combine.

<!-- if you can't perform these tasks due to permissions, please ask a maintainer to do them -->
## Tasks
- [ ] **request a review from someone specific**, to avoid making the maintainers review every PR
- [x] add a build milestone, i.e. `Build 11.3` (use the [latest build](https://github.com/spacetelescope/jwst/milestones) if not sure)
- [x] Does this PR change user-facing code / API? (if not, label with `no-changelog-entry-needed`)
  - [ ] write news fragment(s) in `changes/`: `echo "changed something" > changes/<PR#>.<changetype>.rst` (see below for change types) 
  - [ ] update or add relevant tests
  - [ ] update relevant docstrings and / or `docs/` page
  - [ ] [start a regression test](https://github.com/spacetelescope/RegressionTests/actions/workflows/jwst.yml) and include a link to the running job ([click here for instructions](https://github.com/spacetelescope/RegressionTests/blob/main/docs/running_regression_tests.md))
    - [ ] Do truth files need to be updated ("okified")?
      - [ ] **after the reviewer has approved these changes**, run `okify_regtests` to update the truth files
- [ ] if a JIRA ticket exists, [make sure it is resolved properly](https://github.com/spacetelescope/jwst/wiki/How-to-resolve-JIRA-issues)

<details><summary>news fragment change types...</summary>

- ``changes/<PR#>.general.rst``: infrastructure or miscellaneous change
- ``changes/<PR#>.docs.rst``
- ``changes/<PR#>.stpipe.rst``
- ``changes/<PR#>.datamodels.rst``
- ``changes/<PR#>.scripts.rst``
- ``changes/<PR#>.set_telescope_pointing.rst``
- ``changes/<PR#>.pipeline.rst``

## stage 1
- ``changes/<PR#>.group_scale.rst``
- ``changes/<PR#>.dq_init.rst``
- ``changes/<PR#>.emicorr.rst``
- ``changes/<PR#>.saturation.rst``
- ``changes/<PR#>.ipc.rst``
- ``changes/<PR#>.firstframe.rst``
- ``changes/<PR#>.lastframe.rst``
- ``changes/<PR#>.reset.rst``
- ``changes/<PR#>.superbias.rst``
- ``changes/<PR#>.refpix.rst``
- ``changes/<PR#>.linearity.rst``
- ``changes/<PR#>.rscd.rst``
- ``changes/<PR#>.persistence.rst``
- ``changes/<PR#>.dark_current.rst``
- ``changes/<PR#>.charge_migration.rst``
- ``changes/<PR#>.jump.rst``
- ``changes/<PR#>.clean_flicker_noise.rst``
- ``changes/<PR#>.ramp_fitting.rst``
- ``changes/<PR#>.gain_scale.rst``

## stage 2
- ``changes/<PR#>.assign_wcs.rst``
- ``changes/<PR#>.badpix_selfcal.rst``
- ``changes/<PR#>.msaflagopen.rst``
- ``changes/<PR#>.nsclean.rst``
- ``changes/<PR#>.imprint.rst``
- ``changes/<PR#>.background.rst``
- ``changes/<PR#>.extract_2d.rst``
- ``changes/<PR#>.master_background.rst``
- ``changes/<PR#>.wavecorr.rst``
- ``changes/<PR#>.srctype.rst``
- ``changes/<PR#>.straylight.rst``
- ``changes/<PR#>.wfss_contam.rst``
- ``changes/<PR#>.flatfield.rst``
- ``changes/<PR#>.fringe.rst``
- ``changes/<PR#>.pathloss.rst``
- ``changes/<PR#>.barshadow.rst``
- ``changes/<PR#>.photom.rst``
- ``changes/<PR#>.pixel_replace.rst``
- ``changes/<PR#>.resample_spec.rst``
- ``changes/<PR#>.residual_fringe.rst``
- ``changes/<PR#>.cube_build.rst``
- ``changes/<PR#>.extract_1d.rst``
- ``changes/<PR#>.resample.rst``

## stage 3
- ``changes/<PR#>.assign_mtwcs.rst``
- ``changes/<PR#>.mrs_imatch.rst``
- ``changes/<PR#>.tweakreg.rst``
- ``changes/<PR#>.skymatch.rst``
- ``changes/<PR#>.exp_to_source.rst``
- ``changes/<PR#>.outlier_detection.rst``
- ``changes/<PR#>.tso_photometry.rst``
- ``changes/<PR#>.stack_refs.rst``
- ``changes/<PR#>.align_refs.rst``
- ``changes/<PR#>.klip.rst``
- ``changes/<PR#>.spectral_leak.rst``
- ``changes/<PR#>.source_catalog.rst``
- ``changes/<PR#>.combine_1d.rst``
- ``changes/<PR#>.ami.rst``

## other
- ``changes/<PR#>.wfs_combine.rst``
- ``changes/<PR#>.white_light.rst``
- ``changes/<PR#>.cube_skymatch.rst``
- ``changes/<PR#>.engdb_tools.rst``
- ``changes/<PR#>.guider_cds.rst``
</details>



Diff:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 312cfb3b9b7..e6b7c9ed312 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -58,7 +58,6 @@ repos:
             jwst/spectral_leak/.* |
             jwst/straylight/.* |
             jwst/tests/.* |
-            jwst/wfs_combine/.* |
             docs/.* |
             jwst/regtest/test_.* |
             .*/tests/test_.*
diff --git a/.ruff.toml b/.ruff.toml
index 81d350f0d66..b999bb426c6 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -39,7 +39,6 @@ exclude = [
     "jwst/scripts/**.py",
     "jwst/spectral_leak/**.py",
     "jwst/straylight/**.py",
-    "jwst/wfs_combine/**.py",
 ]
 
 [lint]
@@ -129,7 +128,6 @@ ignore-fully-untyped = true  # Turn off annotation checking for fully untyped co
 "jwst/scripts/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "jwst/spectral_leak/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "jwst/straylight/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
-"jwst/wfs_combine/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "!jwst/engdblog/**.py" = [ # Package-wide logger formatting is https://jira.stsci.edu/browse/JP-3927
     "G004", # logging-f-string
 ]
diff --git a/jwst/wfs_combine/__init__.py b/jwst/wfs_combine/__init__.py
index b0cab3144ae..91d895c7390 100644
--- a/jwst/wfs_combine/__init__.py
+++ b/jwst/wfs_combine/__init__.py
@@ -1,3 +1,5 @@
+"""Correct effects due to overlapping spectral traces."""
+
 from .wfs_combine_step import WfsCombineStep
 
 __all__ = ["WfsCombineStep"]
diff --git a/jwst/wfs_combine/wfs_combine.py b/jwst/wfs_combine/wfs_combine.py
index 8a18b431cbe..cb71514f42f 100644
--- a/jwst/wfs_combine/wfs_combine.py
+++ b/jwst/wfs_combine/wfs_combine.py
@@ -16,56 +16,50 @@
 
 
 class DataSet:
-    """
-    Two dithered input wavefront sensing images to be combined
-
-    """
+    """Two dithered input wavefront sensing images to be combined."""
 
-    def __init__(self, infile_1, infile_2, outfile, do_refine, flip_dithers, psf_size,
-                 blur_size, n_size):
+    def __init__(
+        self, infile_1, infile_2, outfile, do_refine, flip_dithers, psf_size, blur_size, n_size
+    ):
         """
-        Short Summary
-        -------------
         Assign models to input files.
 
-        Parameters:
-        -----------
-        infile_1: string
+        Parameters
+        ----------
+        infile_1 : str
             First input file
-        infile_2: string
+        infile_2 : str
             Second input file
-        outfile: string
+        outfile : str
             File for combined image
-        do_refine: boolean
+        do_refine : bool
             True if refined offset calculation and application is to be made
-        flip_dithers: boolean
+        flip_dithers : bool
             True will cause the dithers to align in pixel coordinates for different filters
-        psf_size: float
-            size of largest psf
-        blur_size: float
-            amount of smoothing to apply before finding the initial centroid
-        n_size: int
-            size of interpolation box
+        psf_size : float
+            Size of largest psf
+        blur_size : float
+            Amount of smoothing to apply before finding the initial centroid
+        n_size : int
+            Size of interpolation box
         """
-
         if outfile == "":
-            log.error('No output product specified in the association table.')
+            log.error("No output product specified in the association table.")
 
         try:
             self.input_1 = datamodels.open(infile_1)
             self.input_2 = datamodels.open(infile_2)
-        except IOError:
-            log.error('Error creating a model from at least 1 of : %s %s',
-                      infile_1, infile_2)
+        except OSError:
+            log.error("Error creating a model from at least 1 of : %s %s", infile_1, infile_2)
 
         self.do_refine = do_refine
 
         if self.input_1.data.shape != self.input_2.data.shape:
-            log.error('Incompatible sizes for input files')
+            log.error("Incompatible sizes for input files")
 
-        log.info('Output file: %s', outfile)
-        log.info('do_refine: %s', do_refine)
-        log.info('flip_dithers: %s', flip_dithers)
+        log.info("Output file: %s", outfile)
+        log.info("do_refine: %s", do_refine)
+        log.info("flip_dithers: %s", flip_dithers)
         self.file1 = infile_1
         self.file2 = infile_2
         self.off_x = 0
@@ -80,19 +74,13 @@ def __init__(self, infile_1, infile_2, outfile, do_refine, flip_dithers, psf_siz
 
     def do_all(self):
         """
-        Short Summary
-        -------------
-        Execute all tasks for Wave Front Sensing Combination
-
-        Parameters
-        ----------
+        Execute all tasks for Wave Front Sensing Combination.
 
         Returns
         -------
-        new_model: data model object
-            combined input file data
+        new_model : data model object
+            Combined input file data
         """
-
         self.off_x, self.off_y = self.get_wcs_offsets()
 
         # If the shift in x is negative, switch the two images
@@ -101,16 +89,16 @@ def do_all(self):
             self.input_2.close()
             self.input_1 = datamodels.open(self.file2)
             self.input_2 = datamodels.open(self.file1)
-            log.info('File 1 to combine: %s', self.file2)
-            log.info('File 2 to combine: %s', self.file1)
+            log.info("File 1 to combine: %s", self.file2)
+            log.info("File 2 to combine: %s", self.file1)
         else:
-            log.info('File 1 to combine: %s', self.file1)
-            log.info('File 2 to combine: %s', self.file2)
+            log.info("File 1 to combine: %s", self.file1)
+            log.info("File 2 to combine: %s", self.file2)
 
         # Input SCI arrays may have nan's so replace with 0's to prevent
         # later annoyances (hopefully this can be removed later)
-        self.input_1.data[np.isnan(self.input_1.data)] = 0.
-        self.input_2.data[np.isnan(self.input_2.data)] = 0.
+        self.input_1.data[np.isnan(self.input_1.data)] = 0.0
+        self.input_2.data[np.isnan(self.input_2.data)] = 0.0
         im_1_a = self.input_1.copy()  # Aligned image #1 (already aligned)
         im_2_a = self.create_aligned_2()  # Aligned image #2
 
@@ -123,27 +111,27 @@ def do_all(self):
         # Create a new model using the combined arrays...
         new_model = datamodels.ImageModel(data=data_c, dq=dq_c, err=err_c)
         new_model.update(self.input_1)
-        new_model.history.append('Flip dithers = {}'.format(self.flip_dithers))
-        new_model.history.append('WFS_COMBINE refine offset = {}'.format(self.do_refine))
-        new_model.history.append('WFS_COMBINE X offset applied ' + str(self.off_x) + ' pixels ' +
-                                 'actual offset ' + str(round(self.flt_off_x, 2)) + ' pixels')
-        new_model.history.append('WFS_COMBINE Y offset applied ' + str(self.off_y) + ' pixels ' +
-                                 'actual offset ' + str(round(self.flt_off_y, 2)) + ' pixels')
+        new_model.history.append(f"Flip dithers = {self.flip_dithers}")
+        new_model.history.append(f"WFS_COMBINE refine offset = {self.do_refine}")
+        new_model.history.append(
+            f"WFS_COMBINE X offset applied {str(self.off_x)} pixels "
+            f"actual offset {str(round(self.flt_off_x, 2))} pixels"
+        )
+        new_model.history.append(
+            f"WFS_COMBINE Y offset applied {str(self.off_y)} pixels "
+            f"actual offset {str(round(self.flt_off_y, 2))} pixels"
+        )
         return new_model
 
     def create_aligned_2(self):
         """
-        Short Summary
-        -------------
         Align image 2 in image 1's frame.
 
-        Long Summary
-        ------------
         If refined offset determination is selected, do steps 1-7 else do 7 only
         1. Create a smoothed image of the input SCI data of image #1. First
            create an image to smooth by first setting SCI pixels with bad DQ
            values equal to the mean of the good pixels. Then smooth this
-           'repaired' image using a gaussian kernel of size BLUR_SIZE.
+           'repaired' image using a Gaussian kernel of size BLUR_SIZE.
         2. Find the approximate centroid of this PSF, by taking all the pixels
            in this smoothed image that exceed 50% of the maximum of the
            smoothed image, and taking the mean of the coordinates of these
@@ -157,15 +145,11 @@ def create_aligned_2(self):
         6. Add the refined delta offsets to the nominal offsets.
         7. Do final alignment for original (not interpolated) image #2
 
-        Parameters
-        ----------
-
         Returns
         -------
-        model_2_a:  Data Model object
-           aligned model for input image #2
+        model_2_a : ImageModel
+           Aligned model for input image #2
         """
-
         self.off_x, self.off_y = self.get_wcs_offsets()
         log.info(f"x,y offset in integer pixels from WCS: {self.off_x} {self.off_y}")
         if self.do_refine:
@@ -178,7 +162,7 @@ def create_aligned_2(self):
 
             # 1b. Create smoothed image by smoothing this 'repaired' image
             g = gauss_kern(self.blur_size, sizey=None)
-            s_data_1 = convolve(data_1, g, mode='valid')
+            s_data_1 = convolve(data_1, g, mode="valid")
 
             # 2. Find approximate center of PSF in unsmoothed frame by taking
             #    all pixels in smoothed image exceeding 50% of the maximum
@@ -189,8 +173,9 @@ def create_aligned_2(self):
             ctrd_x = wh_data_hi[1].mean() + self.blur_size
             ctrd_y = wh_data_hi[0].mean() + self.blur_size
 
-            log.info('Approximate centroid of image 1 PSF has x,y : %s %s',
-                     round(ctrd_x), round(ctrd_y))
+            log.info(
+                "Approximate centroid of image 1 PSF has x,y : %s %s", round(ctrd_x), round(ctrd_y)
+            )
 
             # 3. Set limits of the subarrays (in frames of input data)
             #    for interpolation by taking this centroid +/- psf_size
@@ -201,23 +186,26 @@ def create_aligned_2(self):
             ymax = int(round(min(self.input_1.data.shape[0], ctrd_y + self.psf_size)))
 
             # 3a. Set subarrays and interpolate over bad pixels
-            data_sub_1 = self.input_1.data[ymin: ymax, xmin: xmax]
-            dq_sub_1 = self.input_1.dq[ymin: ymax, xmin: xmax]
+            data_sub_1 = self.input_1.data[ymin:ymax, xmin:xmax]
+            dq_sub_1 = self.input_1.dq[ymin:ymax, xmin:xmax]
             sci_int_1 = interp_array(data_sub_1, dq_sub_1, self.n_size)
 
-            data_sub_2 = self.input_2.data[ymin: ymax, xmin: xmax]
-            dq_sub_2 = self.input_2.dq[ymin: ymax, xmin: xmax]
+            data_sub_2 = self.input_2.data[ymin:ymax, xmin:xmax]
+            dq_sub_2 = self.input_2.dq[ymin:ymax, xmin:xmax]
             sci_int_2 = interp_array(data_sub_2, dq_sub_2, self.n_size)
 
             # 4. Determine overlap of these interpolated images, and
             #    return nominally aligned, interpolated images
-            sci_nai_1, sci_nai_2 = get_overlap(sci_int_1, sci_int_2,
-                                               self.off_x, self.off_y)
+            sci_nai_1, sci_nai_2 = get_overlap(sci_int_1, sci_int_2, self.off_x, self.off_y)
             # 5. Around this nominal alignment, get refined (delta) offsets
-            ref_del_off_x, ref_del_off_y = calc_refined_offsets(sci_nai_1, sci_nai_2, 0, 0, self.psf_size)
-            log.info('From the refined offsets calculation,'
-                     'the x,y changes in offsets are: %s %s',
-                     round(ref_del_off_x, 2), round(ref_del_off_y, 2))
+            ref_del_off_x, ref_del_off_y = calc_refined_offsets(
+                sci_nai_1, sci_nai_2, 0, 0, self.psf_size
+            )
+            log.info(
+                "From the refined offsets calculation,the x,y changes in offsets are: %s %s",
+                round(ref_del_off_x, 2),
+                round(ref_del_off_y, 2),
+            )
 
             # 6. Add the refined delta offsets to the nominal offsets
             self.flt_off_x = self.off_x + ref_del_off_x
@@ -237,23 +225,17 @@ def create_aligned_2(self):
 
     def apply_final_offsets(self):
         """
-        Short Summary
-        -------------
-        Apply final offsets, aligning each array for image #2 to #1's frame
-
-        Parameters
-        ----------
+        Apply final offsets, aligning each array for image #2 to #1's frame.
 
         Returns
         -------
-        data_2_a: 2D float array
-            aligned SCI array of image #2
-        dq_2_a: 2D int array
-            aligned DQ array of image #2
-        err_2_a: 2D float array
-            aligned ERR array of image #2
+        data_2_a : np.ndarray[float]
+            Aligned SCI array of image #2
+        dq_2_a : np.ndarray[int]
+            Aligned DQ array of image #2
+        err_2_a : np.ndarray[float]
+            Aligned ERR array of image #2
         """
-
         data_2_a = self.do_2d_shifts(self.input_2.data)
         dq_2_a = self.do_2d_shifts(self.input_2.dq)
         err_2_a = self.do_2d_shifts(self.input_2.err)
@@ -262,28 +244,22 @@ def apply_final_offsets(self):
 
     def get_wcs_offsets(self):
         """
-        Short Summary
-        -------------
-        Get the nominal offsets from the WCS information of each of the
-        2 input DataModel objects. From the difference in pointings (in
-        pixels) of the 2 images, round off to the nearest integers as
-        the specifications require that the pointings will differ by exact
-        integers.
+        Get the nominal offsets from the WCS information of each of the 2 input DataModel objects.
 
-        Parameters
-        ----------
+        From the difference in pointings (in pixels) of the 2 images, round off to the nearest
+        integers as the specifications require that the pointings will differ by exact integers.
 
         Returns
         -------
-        off_x: integer
-            difference (#2 -#1) in pointing in pixels in the x-direction
-        off_y: integer
-            difference (#2 -#1) in pointing in pixels in the y-direction
+        off_x : int
+            Difference (#2 -#1) in pointing in pixels in the x-direction
+        off_y : int
+            Difference (#2 -#1) in pointing in pixels in the y-direction
         """
         wcs1 = self.input_1.meta.wcs
         wcs2 = self.input_2.meta.wcs
-        tr1 = wcs1.get_transform('detector', 'world')
-        tr2 = wcs2.get_transform('world', 'detector')
+        tr1 = wcs1.get_transform("detector", "world")
+        tr2 = wcs2.get_transform("world", "detector")
 
         # Get coords of center pixel
         xcen = int(self.input_1.data.shape[1] / 2)
@@ -293,8 +269,7 @@ def get_wcs_offsets(self):
         pixels = tr2(radec[0], radec[1])
         off_x = pixels[0] - xcen
         off_y = pixels[1] - ycen
-        log.info('From the WCS the x,y pixel offsets are: %s %s',
-                 round(off_x, 2), round(off_y, 2))
+        log.info("From the WCS the x,y pixel offsets are: %s %s", round(off_x, 2), round(off_y, 2))
         self.flt_off_x = off_x
         self.flt_off_y = off_y
         off_x = int(round(off_x))  # Offsets required to be integers
@@ -304,9 +279,9 @@ def get_wcs_offsets(self):
 
     def create_combined(self, image1, image2):
         """
-        Short Summary
-        -------------
-        Create combined image from aligned input images. In the combined image:
+        Create combined image from aligned input images.
+
+        In the combined image:
 
         The SCI pixel values are set by:
         1. for pixels that are good (based on DQ) in both images, use their average
@@ -330,21 +305,20 @@ def create_combined(self, image1, image2):
 
         Parameters
         ----------
-        image1: 2D Data Model
-             aligned image from input #1
-        image2: 2D Data Model
-             aligned image from input #2
+        image1 : ImageModel
+             Aligned image from input #1
+        image2 : ImageModel
+             Aligned image from input #2
 
         Returns
         -------
-        data_comb: 2d float array
-            combined SCI array
-        dq_comb: 2d integer array
-            combined DQ array
-        err_comb: 2d float array
-            combined ERR array
+        data_comb : np.ndarray[float]
+            Combined SCI array
+        dq_comb : np.ndarray[int]
+            Combined DQ array
+        err_comb : np.ndarray[float]
+            Combined ERR array
         """
-
         data1 = image1.data.astype(float)
         data2 = image2.data.astype(float)
         dq1 = image1.dq.copy()
@@ -377,7 +351,7 @@ def create_combined(self, image1, image2):
         err_comb[good2 & bad1] = err2[good2 & bad1]
 
         data_diff = np.zeros_like(data1)
-        data_diff[good1 & good2] = (data1[good1 & good2] - data2[good1 & good2])
+        data_diff[good1 & good2] = data1[good1 & good2] - data2[good1 & good2]
         data_diff[good1 & bad2] = 0
         data_diff[good2 & bad1] = 0
 
@@ -385,20 +359,20 @@ def create_combined(self, image1, image2):
 
     def do_2d_shifts(self, a):
         """
-        Short Summary
-        -------------
-        Create 2d output array by shifting 2d array input by (off_x, off_y),
-        where output has same dimensions as input.
+        Create 2d output array by shifting 2d array input by (off_x, off_y).
+
+        The output will have the same dimensions as the input.
+
         Parameters
         ----------
-        a: 2d float array
-            input array
+        a : np.ndarray[float]
+            Input array
+
         Returns
         -------
-        b: 2d float array
-            shifted array of input a
+        b : np.ndarray[float]
+            Shifted array of input a
         """
-
         ai_x, af_x = get_final_index_range(self.off_x, a.shape[1])
         ai_y, af_y = get_final_index_range(self.off_y, a.shape[0])
 
@@ -416,25 +390,25 @@ def do_2d_shifts(self, a):
 
 def get_final_index_range(offset, length):
     """
-    Short Summary
-    -------------
-    Get the initial and final indices for the given offset and array length:
+    Get the initial and final indices for the given offset and array length.
+
     For offset <= 0: i = 0,  f = length - abs(offset)
     For offset > 0: i = offset,  f = length
+
     Parameters
     ----------
-    offset: integer
-        offset
-    length: integer
-        length of (1D) array
+    offset : int
+        Offset
+    length : int
+        Length of (1D) array
+
     Returns
     -------
-    i: integer
-        initial index
-    f: integer
-        final index
+    i : int
+        Initial index
+    f : int
+        Final index
     """
-
     i = int((abs(offset) + offset) / 2)
     f = length + int((-abs(offset) + offset) / 2)
 
@@ -443,22 +417,19 @@ def get_final_index_range(offset, length):
 
 def gauss_kern(size, sizey=None):
     """
-    Short Summary
-    -------------
-    Returns a normalized 2D gauss kernel array for convolution.
+    Return a normalized 2D Gaussian kernel array for convolution.
 
     Parameters
     ----------
-    size: int
-        size of gaussian kernel in x_dim
-    sizey: int
-        sizey of gaussian kernel in y_dim
+    size : int
+        Size of Gaussian kernel in x_dim
+    sizey : int
+        Size of Gaussian kernel in y_dim
 
     Returns
     -------
-    g/g.sum(): 2D float array
-        normalized 2D gauss kernel array
-
+    g/g.sum() : 2D float array
+        Normalized 2D Gaussian kernel array
     """
     size = int(size)
     if not sizey:
@@ -466,7 +437,7 @@ def gauss_kern(size, sizey=None):
     else:
         sizey = int(sizey)
 
-    x, y = np.mgrid[-size:size + 1, -sizey:sizey + 1]
+    x, y = np.mgrid[-size : size + 1, -sizey : sizey + 1]
     g = np.exp(-(x**2 / float(size) + y**2 / float(sizey)))
 
     return g / g.sum()
@@ -474,27 +445,26 @@ def gauss_kern(size, sizey=None):
 
 def interp_array(sci_data, dq_data, n_size):
     """
-    Short Summary
-    -------------
+    Bilinear interpolation over the SCI values.
+
     For bad DQ values as given by the input DQ array, do a
     bilinear interpolation over the corresponding SCI values and return
     the interpolated SCI array.
 
     Parameters
     ----------
-    sci_data: 2D float array
-        original SCI image to interpolate over
-    dq_data: 2D int array
-        corresponding DQ image
-    n_size: int
-        size of the interpolation box
+    sci_data : np.ndarray[float]
+        Original SCI image to interpolate over
+    dq_data : np.ndarray[int]
+        Corresponding DQ image
+    n_size : int
+        Size of the interpolation box
 
     Returns
     -------
-    sci_data: 2D float array
-        interpolated SCI image
+    sci_data : 2D float array
+        Interpolated SCI image
     """
-
     wh_bad_dq = np.where(np.bitwise_and(dq_data, DO_NOT_USE))
     num_bad_dq = len(wh_bad_dq[0])
 
@@ -511,40 +481,36 @@ def interp_array(sci_data, dq_data, n_size):
         y = bad_dq[jj][0]
 
         # Linearly interpolate using scipy's griddata to fill in missing value
-        sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method='linear').item()
+        sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method="linear").item()
 
         # For those interpolations just done that result in a nan (because
         #    there may be too few pixels), check and redo with 'nearest'
         if np.isnan(sci_data[y, x]):
-            sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method='nearest').item()
+            sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method="nearest").item()
 
     return sci_data
 
 
 def create_griddata_array(sci_data, pixel, n_size):
     """
-    Short Summary
-    -------------
-    Create interpolation array for input to scipy's griddata. This array
-    consists of the coordinates and the pixel value for each of
+    Create interpolation array for input to scipy's griddata.
+
+    This array consists of the coordinates and the pixel value for each of
     pixels neighboring the input pixel.
 
     Parameters
     ----------
-    sci_data: 2D float array
-        original SCI image
-
-    pixel: int, int
-        y, x coordinates of pixel to interpolate over
-
-    n_size: int
-        size of the interpolation box
+    sci_data : np.ndarray[float]
+        Original SCI image
+    pixel : tuple(int,int)
+        Coordinates y, x  of pixel to interpolate over
+    n_size : int
+        Size of the interpolation box
 
     Returns
     -------
-    interp_arr: int, int, float
-        pixel coords, pixel value for each pixel neighboring the input pixel
-
+    interp_arr : np.ndarray([int,int,float])
+        Pixel coords, pixel value for each pixel neighboring the input pixel
     """
     xdim = sci_data.shape[1]
     ydim = sci_data.shape[0]
@@ -577,24 +543,24 @@ def create_griddata_array(sci_data, pixel, n_size):
 
 def get_index_range(offset, length):
     """
-    Short Summary
-    -------------
-    Get the initial and final indices for the given offset and array length:
+    Get the initial and final indices for the given offset and array length.
+
     For offset <= 0: i = 0,  f = length - abs(offset)
     For offset > 0: i = offset,  f = length
 
     Parameters
     ----------
-    offset: integer
-        offset
-
-    length: integer
-        length of (1D) array
+    offset : int
+        Offset
+    length : int
+        Length of (1D) array
 
     Returns
     -------
-        initial index : integer
-        final index : integer
+    initial index : int
+        Initial index for the given offset
+    final index : int
+        Final index for the given offset
     """
     if offset > 0:
         initial_1 = 0
@@ -612,18 +578,13 @@ def get_index_range(offset, length):
 
 def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
     """
-    Short Summary
-    -------------
-    Apply nominal offsets (of image #2 relative to image #1) to determine
-    the overlap in interpolated images.
-
-    Long Summary
-    -------------
-    Apply nominal offsets (of image #2 relative to image #1) to determine
-    the overlap in interpolated images.  The resulting two subarrays are
-    the pixels common to both. In other words, image #2 is shifted onto
-    the frame of image #1, with the dimensions of the subarrays equal to
-    the dimensions of the overlap.
+    Apply nominal offsets to determine the overlap in interpolated images.
+
+    Nominal offsets are calculated as image #2 relative to image #1.
+
+    The resulting two subarrays are the pixels common to both. In other words,
+    image #2 is shifted onto the frame of image #1, with the dimensions of
+    the subarrays equal to the dimensions of the overlap.
 
     To illustrate with pseudocode for a 1D array with length 'length': for
     a given offset 'off', the resulting initial and final indices, and
@@ -643,23 +604,22 @@ def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
 
     Parameters
     ----------
-    sci_int_1: 2d float array
-        interpolated SCI array for image 1
-    sci_int_2: 2d float array
-        interpolated SCI array for image 2
-    nom_off_x: integer
-        nominal offset in x-direction
-    nom_off_y: integer
-        nominal offset in y-direction
+    sci_int_1 : np.ndarray[float]
+        Interpolated SCI array for image 1
+    sci_int_2 : np.ndarray[float]
+        Interpolated SCI array for image 2
+    nom_off_x : int
+        Nominal offset in x-direction
+    nom_off_y : int
+        Nominal offset in y-direction
 
     Returns
     -------
-    sub_1: 2d float array
-        overlapping subarray for interpolated image 1
-    sub_2: 2d float array
-        overlapping subarray for interpolated image 2
+    sub_1 : np.ndarray[float]
+        Overlapping subarray for interpolated image 1
+    sub_2 : np.ndarray[float]
+        Overlapping subarray for interpolated image 2
     """
-
     # From the nominal offsets, determine array indices to shift image #2
     #     onto frame #1
     initial_x_1, final_x_1, initial_x_2, final_x_2 = get_index_range(nom_off_x, sci_int_2.shape[1])
@@ -673,35 +633,31 @@ def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
 
 def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
     """
-    Short Summary
-    -------------
-    Get the overlap of the 2 images (based on the offsets), and
-    calculate the two dimensional cross correlation image between 2 image subarrays.
-    Then we slice on the a subarray around the peak of the cross correlation image and
-    find the first moment. The first moment provides a high S/N measurement of the offset
-    between the two images.
+    Get overlap of the two images and calculate 2D cross correlation between the two subarrays.
 
+    The overlap image is based on the offsets. Then we slice on a subarray around the peak of
+    the cross correlation image and find the first moment. The first moment provides a high
+    S/N measurement of the offset between the two images.
 
     Parameters
     ----------
-    sci_nai_1: 2d float array
-        nominally aligned, interpolated SCI subarray for image 1
-    sci_nai_2: 2d float array
-        nominally aligned, interpolated SCI subarray for image 2
-    off_x: integer
-        offset in x-direction
-    off_y: integer
-        offset in y-direction
-    psf_size: integer
+    sci_nai_1 : np.ndarray[float]
+        Nominally aligned, interpolated SCI subarray for image 1
+    sci_nai_2 : np.ndarray[float]
+        Nominally aligned, interpolated SCI subarray for image 2
+    off_x : int
+        Offset in x-direction
+    off_y : int
+        Offset in y-direction
+    psf_size : int
         The worst case PSF size
 
     Returns
     -------
-    refined_x: float
+    refined_x : float
         The refined value of the x offset
-    refined_y: float
+    refined_y : float
         The refined value of the y offset
-
     """
     centroid_size = 3
     sub_1, sub_2 = get_overlap(sci_nai_1, sci_nai_2, off_x, off_y)
@@ -709,8 +665,8 @@ def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
 
     # Raise (fatal) exception if there are no overlapping pixels
     if num_pix == 0:
-        log.error('Applying offsets to image #2 results in 0 overlapping pix')
-        raise RuntimeWarning('No overlapping pixels in 2 images')
+        log.error("Applying offsets to image #2 results in 0 overlapping pixels")
+        raise RuntimeWarning("No overlapping pixels in 2 images")
 
     # Set limits for subarrays, centered on the overlap and +/- psf half width,
     #   taking edges into account
@@ -728,16 +684,19 @@ def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
     sub_1_sub = sub_1[ymin:ymax, xmin:xmax]
     sub_2_sub = sub_2[ymin:ymax, xmin:xmax]
     # Create the cross correlation image
-    cross_cor = correlate2d(sub_2_sub - gaussian_filter(sub_2_sub, 5), sub_1_sub -
-                            gaussian_filter(sub_1_sub, 5))
+    cross_cor = correlate2d(
+        sub_2_sub - gaussian_filter(sub_2_sub, 5), sub_1_sub - gaussian_filter(sub_1_sub, 5)
+    )
     maximum_pixel = np.unravel_index(np.argmax(cross_cor), cross_cor.shape)
 
     ymax = maximum_pixel[0] - sub_1_sub.shape[0] + 1
     xmax = maximum_pixel[1] - sub_1_sub.shape[1] + 1
-    # Slice out a box center on the peak of the cross correlation image. The centroid of this box will give a
-    # accurate estimate of the x and y offsets.
-    central_cutout = cross_cor[maximum_pixel[0] - centroid_size:maximum_pixel[0] + centroid_size + 1,
-                               maximum_pixel[1] - centroid_size:maximum_pixel[1] + centroid_size + 1]
+    # Slice out a box center on the peak of the cross correlation image. The
+    # centroid of this box will give an accurate estimate of the x and y offsets.
+    central_cutout = cross_cor[
+        maximum_pixel[0] - centroid_size : maximum_pixel[0] + centroid_size + 1,
+        maximum_pixel[1] - centroid_size : maximum_pixel[1] + centroid_size + 1,
+    ]
     centroid = center_of_mass(central_cutout)
     refined_x = xmax + centroid[1] - centroid_size
     refined_y = ymax + centroid[0] - centroid_size
diff --git a/jwst/wfs_combine/wfs_combine_step.py b/jwst/wfs_combine/wfs_combine_step.py
index c941a3628d9..c1cdef4d484 100644
--- a/jwst/wfs_combine/wfs_combine_step.py
+++ b/jwst/wfs_combine/wfs_combine_step.py
@@ -1,4 +1,4 @@
-import os
+from pathlib import Path
 
 from jwst.datamodels import ModelContainer
 
@@ -9,10 +9,7 @@
 
 
 class WfsCombineStep(Step):
-
-    """
-    This step combines pairs of dithered PSF images
-    """
+    """Combine pairs of dithered PSF images."""
 
     class_alias = "calwebb_wfs-image3"
 
@@ -23,42 +20,59 @@ class WfsCombineStep(Step):
         blur_size = integer(default=10)
         n_size = integer(default=2)
         suffix = string(default="wfscmb")
-    """ # noqa: E501
+    """  # noqa: E501
+
+    def make_output_path(self, basepath, *args, **kwargs):  # noqa:  ARG002
+        """
+        Filename formatting bypass in stpipe.
 
-    def make_output_path(self, basepath, *args, **kwargs):
-        # bypass all stpipe filename formatting
+        Returns
+        -------
+        basepath : str
+            Basename of the file
+        """
         return basepath
 
     def process(self, input_table):
-
-        self.suffix = 'wfscmb'
+        """
+        Combine image pairs.
+
+        Returns
+        -------
+        output_container : ModelContainer
+            Model container with all combined data
+        """
+        self.suffix = "wfscmb"
         self.output_use_model = True
 
         # Load the input ASN table
         asn_table = self.load_as_level3_asn(input_table)
 
-        self.log.info('Using input table: %s', input_table)
-        self.log.info('The number of pairs of input files: %g', len(asn_table['products']))
+        self.log.info("Using input table: %s", input_table)
+        self.log.info("The number of pairs of input files: %g", len(asn_table["products"]))
 
         output_container = ModelContainer()
 
         # Process each pair of input images listed in the association table
-        for which_set in asn_table['products']:
-
+        for which_set in asn_table["products"]:
             # Get the list of science members in this pair
             science_members = [
-                member
-                for member in which_set['members']
-                if member['exptype'].lower() == 'science'
+                member for member in which_set["members"] if member["exptype"].lower() == "science"
             ]
-            infile_1 = science_members[0]['expname']
-            infile_2 = science_members[1]['expname']
-            outfile = which_set['name']
+            infile_1 = science_members[0]["expname"]
+            infile_2 = science_members[1]["expname"]
+            outfile = which_set["name"]
 
             # Create the step instance
             wfs = wfs_combine.DataSet(
-                infile_1, infile_2, outfile, self.do_refine, self.flip_dithers, self.psf_size,
-                self.blur_size, self.n_size
+                infile_1,
+                infile_2,
+                outfile,
+                self.do_refine,
+                self.flip_dithers,
+                self.psf_size,
+                self.blur_size,
+                self.n_size,
             )
 
             # Do the processing
@@ -69,11 +83,13 @@ def process(self, input_table):
             wfs.input_2.close()
 
             # Update necessary meta info in the output
-            output_model.meta.cal_step.wfs_combine = 'COMPLETE'
-            output_model.meta.asn.pool_name = asn_table['asn_pool']
-            output_model.meta.asn.table_name = os.path.basename(input_table)
+            output_model.meta.cal_step.wfs_combine = "COMPLETE"
+            output_model.meta.asn.pool_name = asn_table["asn_pool"]
+            output_model.meta.asn.table_name = Path(input_table).name
             # format the filename here
-            output_model.meta.filename = which_set['name'].format(suffix=self.suffix) + self.output_ext
+            output_model.meta.filename = (
+                which_set["name"].format(suffix=self.suffix) + self.output_ext
+            )
 
             output_container.append(output_model)
 


Reviews:
user: login: emolter
body: Thanks Maria, looks good overall.  I left some comments
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T19:15:11Z
user: login: penaguerrero
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T20:23:47Z
user: login: emolter
body: All my comments have been addressed, thanks Maria!  Would you please start a regression test run?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T15:17:09Z
user: login: emolter
body: I assume the DHS mode regtest failures are unrelated, so LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T13:04:15Z
user: login: tapastro
body: Looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:27:58Z

Review comments:
diff_hunk: @@ -123,22 +111,30 @@ def do_all(self):
         # Create a new model using the combined arrays...
         new_model = datamodels.ImageModel(data=data_c, dq=dq_c, err=err_c)
         new_model.update(self.input_1)
-        new_model.history.append('Flip dithers = {}'.format(self.flip_dithers))
-        new_model.history.append('WFS_COMBINE refine offset = {}'.format(self.do_refine))
-        new_model.history.append('WFS_COMBINE X offset applied ' + str(self.off_x) + ' pixels ' +
-                                 'actual offset ' + str(round(self.flt_off_x, 2)) + ' pixels')
-        new_model.history.append('WFS_COMBINE Y offset applied ' + str(self.off_y) + ' pixels ' +
-                                 'actual offset ' + str(round(self.flt_off_y, 2)) + ' pixels')
+        new_model.history.append(f"Flip dithers = {self.flip_dithers}")
+        new_model.history.append(f"WFS_COMBINE refine offset = {self.do_refine}")
+        new_model.history.append(
+            "WFS_COMBINE X offset applied "
+            + str(self.off_x)
+            + " pixels "
+            + "actual offset "
+            + str(round(self.flt_off_x, 2))
+            + " pixels"
+        )
+        new_model.history.append(
+            "WFS_COMBINE Y offset applied "
+            + str(self.off_y)
+            + " pixels "
+            + "actual offset "
+            + str(round(self.flt_off_y, 2))
+            + " pixels"
+        )
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: These might be easier to read if they're turned into f-strings
created_at: 2025-03-20T18:24:32Z
author_association: COLLABORATOR
diff_hunk: @@ -330,21 +313,20 @@ def create_combined(self, image1, image2):
 
         Parameters
         ----------
-        image1: 2D Data Model
-             aligned image from input #1
-        image2: 2D Data Model
-             aligned image from input #2
+        image1 : 2D Data Model
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: what datamodel types are supported here?
created_at: 2025-03-20T18:26:15Z
author_association: COLLABORATOR
diff_hunk: @@ -330,21 +313,20 @@ def create_combined(self, image1, image2):
 
         Parameters
         ----------
-        image1: 2D Data Model
-             aligned image from input #1
-        image2: 2D Data Model
-             aligned image from input #2
+        image1 : 2D Data Model
+             Aligned image from input #1
+        image2 : 2D Data Model
+             Aligned image from input #2
 
         Returns
         -------
-        data_comb: 2d float array
-            combined SCI array
-        dq_comb: 2d integer array
-            combined DQ array
-        err_comb: 2d float array
-            combined ERR array
+        data_comb : 2d float array
+            Combined SCI array
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: are these numpy arrays?  It might be good to specify, e.g. `data_comb : np.ndarray[float]`
created_at: 2025-03-20T18:26:51Z
author_association: COLLABORATOR
diff_hunk: @@ -443,58 +425,54 @@ def get_final_index_range(offset, length):
 
 def gauss_kern(size, sizey=None):
     """
-    Short Summary
-    -------------
-    Returns a normalized 2D gauss kernel array for convolution.
+    Return a normalized 2D gauss kernel array for convolution.
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: ```suggestion
    Return a normalized 2D Gaussian kernel array for convolution.
```
created_at: 2025-03-20T18:27:26Z
author_association: COLLABORATOR
diff_hunk: @@ -443,58 +425,54 @@ def get_final_index_range(offset, length):
 
 def gauss_kern(size, sizey=None):
     """
-    Short Summary
-    -------------
-    Returns a normalized 2D gauss kernel array for convolution.
+    Return a normalized 2D gauss kernel array for convolution.
 
     Parameters
     ----------
-    size: int
-        size of gaussian kernel in x_dim
-    sizey: int
-        sizey of gaussian kernel in y_dim
+    size : int
+        Size of gaussian kernel in x_dim
+    sizey : int
+        Size y of gaussian kernel in y_dim
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: ```suggestion
        Size of gaussian kernel in y_dim
```
created_at: 2025-03-20T18:27:41Z
author_association: COLLABORATOR
diff_hunk: @@ -511,40 +489,36 @@ def interp_array(sci_data, dq_data, n_size):
         y = bad_dq[jj][0]
 
         # Linearly interpolate using scipy's griddata to fill in missing value
-        sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method='linear').item()
+        sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method="linear").item()
 
         # For those interpolations just done that result in a nan (because
         #    there may be too few pixels), check and redo with 'nearest'
         if np.isnan(sci_data[y, x]):
-            sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method='nearest').item()
+            sci_data[y, x] = griddata(ga[:, 0:2], ga[:, 2], [(y, x)], method="nearest").item()
 
     return sci_data
 
 
 def create_griddata_array(sci_data, pixel, n_size):
     """
-    Short Summary
-    -------------
-    Create interpolation array for input to scipy's griddata. This array
-    consists of the coordinates and the pixel value for each of
+    Create interpolation array for input to scipy's griddata.
+
+    This array consists of the coordinates and the pixel value for each of
     pixels neighboring the input pixel.
 
     Parameters
     ----------
-    sci_data: 2D float array
-        original SCI image
-
-    pixel: int, int
-        y, x coordinates of pixel to interpolate over
-
-    n_size: int
-        size of the interpolation box
+    sci_data : 2D float array
+        Original SCI image
+    pixel : int, int
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: ```suggestion
    pixel : tuple(int,int)
```
created_at: 2025-03-20T18:28:29Z
author_association: COLLABORATOR
diff_hunk: @@ -673,44 +641,42 @@ def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
 
 def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
     """
-    Short Summary
-    -------------
-    Get the overlap of the 2 images (based on the offsets), and
-    calculate the two dimensional cross correlation image between 2 image subarrays.
-    Then we slice on the a subarray around the peak of the cross correlation image and
+    Get overlap of the 2 images and calculate 2D cross correlation image between 2 image subarrays.
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: ```suggestion
    Get overlap of the two images and calculate 2D cross correlation image between the two image subarrays.
```
created_at: 2025-03-20T18:29:15Z
author_association: COLLABORATOR
diff_hunk: @@ -673,44 +641,42 @@ def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
 
 def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
     """
-    Short Summary
-    -------------
-    Get the overlap of the 2 images (based on the offsets), and
-    calculate the two dimensional cross correlation image between 2 image subarrays.
-    Then we slice on the a subarray around the peak of the cross correlation image and
+    Get overlap of the 2 images and calculate 2D cross correlation image between 2 image subarrays.
+
+    The overlap is based on the offsets.
+
+    Then we slice on a subarray around the peak of the cross correlation image and
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: These two sentences are now non-sequitors, is it possible to re-work this description to contain all the information but avoid this?
created_at: 2025-03-20T18:29:53Z
author_association: COLLABORATOR
diff_hunk: @@ -673,44 +641,42 @@ def get_overlap(sci_int_1, sci_int_2, nom_off_x, nom_off_y):
 
 def calc_refined_offsets(sci_nai_1, sci_nai_2, off_x, off_y, psf_size):
     """
-    Short Summary
-    -------------
-    Get the overlap of the 2 images (based on the offsets), and
-    calculate the two dimensional cross correlation image between 2 image subarrays.
-    Then we slice on the a subarray around the peak of the cross correlation image and
+    Get overlap of the 2 images and calculate 2D cross correlation image between 2 image subarrays.
+
+    The overlap is based on the offsets.
+
+    Then we slice on a subarray around the peak of the cross correlation image and
     find the first moment. The first moment provides a high S/N measurement of the offset
     between the two images.
 
-
     Parameters
     ----------
-    sci_nai_1: 2d float array
-        nominally aligned, interpolated SCI subarray for image 1
-    sci_nai_2: 2d float array
-        nominally aligned, interpolated SCI subarray for image 2
-    off_x: integer
-        offset in x-direction
-    off_y: integer
-        offset in y-direction
-    psf_size: integer
+    sci_nai_1 : 2d float array
+        Nominally aligned, interpolated SCI subarray for image 1
+    sci_nai_2 : 2d float array
+        Nominally aligned, interpolated SCI subarray for image 2
+    off_x : int
+        Offset in x-direction
+    off_y : int
+        Offset in y-direction
+    psf_size : int
         The worst case PSF size
 
     Returns
     -------
-    refined_x: float
+    refined_x : float
         The refined value of the x offset
-    refined_y: float
+    refined_y : float
         The refined value of the y offset
-
     """
     centroid_size = 3
     sub_1, sub_2 = get_overlap(sci_nai_1, sci_nai_2, off_x, off_y)
     num_pix = sub_1.shape[0] * sub_1.shape[1]  # Number of overlapping pixels
 
     # Raise (fatal) exception if there are no overlapping pixels
     if num_pix == 0:
-        log.error('Applying offsets to image #2 results in 0 overlapping pix')
-        raise RuntimeWarning('No overlapping pixels in 2 images')
+        log.error("Applying offsets to image #2 results in 0 overlapping pix")
path: jwst/wfs_combine/wfs_combine.py
user: login: emolter
body: ```suggestion
        log.error("Applying offsets to image #2 results in 0 overlapping pixels.")
```
created_at: 2025-03-20T19:11:22Z
author_association: COLLABORATOR
diff_hunk: @@ -23,42 +20,59 @@ class WfsCombineStep(Step):
         blur_size = integer(default=10)
         n_size = integer(default=2)
         suffix = string(default="wfscmb")
-    """ # noqa: E501
+    """  # noqa: E501
+
+    def make_output_path(self, basepath, *args, **kwargs):  # noqa:  ARG002
+        """
+        Ensure to bypass all stpipe filename formatting.
path: jwst/wfs_combine/wfs_combine_step.py
user: login: emolter
body: ```suggestion
        Bypass all stpipe filename formatting.
```
Does the checker not like using bypass as a verb?
created_at: 2025-03-20T19:12:16Z
author_association: COLLABORATOR
diff_hunk: @@ -23,42 +20,59 @@ class WfsCombineStep(Step):
         blur_size = integer(default=10)
         n_size = integer(default=2)
         suffix = string(default="wfscmb")
-    """ # noqa: E501
+    """  # noqa: E501
+
+    def make_output_path(self, basepath, *args, **kwargs):  # noqa:  ARG002
+        """
+        Ensure to bypass all stpipe filename formatting.
 
-    def make_output_path(self, basepath, *args, **kwargs):
-        # bypass all stpipe filename formatting
+        Returns
+        -------
+        basepath : str
+            Basename of the file
+        """
         return basepath
 
     def process(self, input_table):
-
-        self.suffix = 'wfscmb'
+        """
+        Do combination of image pairs.
path: jwst/wfs_combine/wfs_combine_step.py
user: login: emolter
body: ```suggestion
        Combine image pairs.
```
created_at: 2025-03-20T19:12:50Z
author_association: COLLABORATOR
diff_hunk: @@ -23,42 +20,59 @@ class WfsCombineStep(Step):
         blur_size = integer(default=10)
         n_size = integer(default=2)
         suffix = string(default="wfscmb")
-    """ # noqa: E501
+    """  # noqa: E501
+
+    def make_output_path(self, basepath, *args, **kwargs):  # noqa:  ARG002
+        """
+        Ensure to bypass all stpipe filename formatting.
 
-    def make_output_path(self, basepath, *args, **kwargs):
-        # bypass all stpipe filename formatting
+        Returns
+        -------
+        basepath : str
+            Basename of the file
+        """
         return basepath
 
     def process(self, input_table):
-
-        self.suffix = 'wfscmb'
+        """
+        Do combination of image pairs.
+
+        Returns
+        -------
+        output_container : model container
path: jwst/wfs_combine/wfs_combine_step.py
user: login: emolter
body: ```suggestion
        output_container : ModelContainer
```
created_at: 2025-03-20T19:13:00Z
author_association: COLLABORATOR
diff_hunk: @@ -69,11 +83,13 @@ def process(self, input_table):
             wfs.input_2.close()
 
             # Update necessary meta info in the output
-            output_model.meta.cal_step.wfs_combine = 'COMPLETE'
-            output_model.meta.asn.pool_name = asn_table['asn_pool']
-            output_model.meta.asn.table_name = os.path.basename(input_table)
+            output_model.meta.cal_step.wfs_combine = "COMPLETE"
+            output_model.meta.asn.pool_name = asn_table["asn_pool"]
+            output_model.meta.asn.table_name = os.path.basename(input_table)  # noqa: PTH119
path: jwst/wfs_combine/wfs_combine_step.py
user: login: emolter
body: I know I said on the other PR to avoid making pathlib changes, but this one looks small and simple enough that I think it's ok to put it in there.  Plus the test coverage is probably better than in the scripts - I think our regtests should catch if there are any differences in the output metadata.
created_at: 2025-03-20T19:14:42Z
author_association: COLLABORATOR
diff_hunk: @@ -23,42 +20,59 @@ class WfsCombineStep(Step):
         blur_size = integer(default=10)
         n_size = integer(default=2)
         suffix = string(default="wfscmb")
-    """ # noqa: E501
+    """  # noqa: E501
+
+    def make_output_path(self, basepath, *args, **kwargs):  # noqa:  ARG002
+        """
+        Ensure to bypass all stpipe filename formatting.
path: jwst/wfs_combine/wfs_combine_step.py
user: login: penaguerrero
body: that's exactly what happened :P 
created_at: 2025-03-21T20:23:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-20T17:03:19Z
body: ## [Codecov](https://app.codecov.io/gh/spacetelescope/jwst/pull/9307?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) Report
Attention: Patch coverage is `84.31373%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 74.62%. Comparing base [(`d992a51`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/d992a5173bc2a6ae7cb5ec3f3c08816a5e0b3fa7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) to head [(`95f1803`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/95f18033078e058b0eaca045811b94ec3bd69541?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).
> Report is 760 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/spacetelescope/jwst/pull/9307?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) | Patch % | Lines |
|---|---|---|
| [jwst/wfs\_combine/wfs\_combine.py](https://app.codecov.io/gh/spacetelescope/jwst/pull/9307?src=pr&el=tree&filepath=jwst%2Fwfs_combine%2Fwfs_combine.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-andzdC93ZnNfY29tYmluZS93ZnNfY29tYmluZS5weQ==) | 78.94% | [8 Missing :warning: ](https://app.codecov.io/gh/spacetelescope/jwst/pull/9307?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #9307   +/-   ##
=======================================
  Coverage   74.62%   74.62%           
=======================================
  Files         369      369           
  Lines       37149    37149           
=======================================
  Hits        27724    27724           
  Misses       9425     9425           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/spacetelescope/jwst/pull/9307?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: penaguerrero
created_at: 2025-03-24T20:05:02Z
body: reg test running: https://github.com/spacetelescope/RegressionTests/actions/runs/14044806984
author_association: CONTRIBUTOR