Repository: spacetelescope/jwst

Issue:
NIRSpec imaging products have no bounding box assigned to WCS
_Issue [JP-3343](https://jira.stsci.edu/browse/JP-3343) was created on JIRA by [Tyler Pauly](https://jira.stsci.edu/secure/ViewProfile.jspa?name=tpauly):_

From a Help Desk ticket (from [Charles Proffitt](https://jira.stsci.edu/secure/ViewProfile.jspa?name=proffitt)), it was noted that NIRSpec imaging products have no FITS SIP parameters assigned. This was tracked down to the WCS not having a bounding_box assigned. Simple fix is available.

Comments:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index b84da22e0..7852f2583 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,6 +6,9 @@ assign_wcs
 
 - Use isinstance instead of comparison with a type for lamp_mode inspection [#7801]
 
+- Save bounding box to imaging WCS matching the shape of the data, for datamodels
+  without a defined bounding box. [#7809]
+
 associations
 ------------
 
@@ -93,6 +96,13 @@ residual_fringe
 - Use scipy.interpolate.BSpline instead of astropy.modeling.Spline1D in
   residual_fringe fitting utils [#7764]
 
+undersampling_correction
+------------------------
+
+- Changed default signal threshold, added efficient routine to flag neighborhood
+  pixels, added new unit test, improved earlier unit tests, updated docs. [#7740]
+  
+
 1.11.4 (2023-08-14)
 ===================
 
diff --git a/docs/jwst/pipeline/calwebb_detector1.rst b/docs/jwst/pipeline/calwebb_detector1.rst
index ca126b4b5..07e78aa44 100644
--- a/docs/jwst/pipeline/calwebb_detector1.rst
+++ b/docs/jwst/pipeline/calwebb_detector1.rst
@@ -71,10 +71,10 @@ on either the first group or frame zero pixel values.
 | :ref:`dark_current <dark_current_step>`                              | |check| | |check| | :ref:`dark_current <dark_current_step>` | |check| | |check| |
 +----------------------------------------------------------------------+---------+---------+-----------------------------------------+---------+---------+
 |                                                                      |         |         | :ref:`refpix <refpix_step>`             | |check| | |check| |
-+----------------------------------------------------------------------+---------+---------+----------------------+------------------+---------+---------+
-| :ref:`jump <jump_step>`                                              | |check| | |check| | :ref:`jump <jump_step>`                 | |check| | |check| |
 +----------------------------------------------------------------------+---------+---------+-----------------------------------------+---------+---------+
 | :ref:`undersampling_correction <undersampling_correction_step>` [3]_ | |check| |         |                                         |         |         |
++----------------------------------------------------------------------+---------+---------+----------------------+------------------+---------+---------+
+| :ref:`jump <jump_step>`                                              | |check| | |check| | :ref:`jump <jump_step>`                 | |check| | |check| |
 +----------------------------------------------------------------------+---------+---------+-----------------------------------------+---------+---------+
 | :ref:`ramp_fitting <ramp_fitting_step>`                              | |check| | |check| | :ref:`ramp_fitting <ramp_fitting_step>` | |check| | |check| |
 +----------------------------------------------------------------------+---------+---------+-----------------------------------------+---------+---------+
diff --git a/docs/jwst/undersampling_correction/description.rst b/docs/jwst/undersampling_correction/description.rst
index 4b9387632..3a128b339 100644
--- a/docs/jwst/undersampling_correction/description.rst
+++ b/docs/jwst/undersampling_correction/description.rst
@@ -12,11 +12,15 @@ a pixel. When the peak pixels of such stars approach the saturation level, they
 significant :ref:`charge migration <charge_migration>`:
 the spilling of charge from a saturated pixel into its neighboring pixels. This charge migration 
 causes group-to-group differences to decrease significantly once the signal level is greater than 
-~30,000 ADU. As a result, the last several groups of these ramps get flagged by the ``jump`` step. 
-The smaller number of groups used for these pixels in the ``ramp_fitting`` step results in them having 
+~25,000 ADU.  As a result, the last several groups of these ramps will be flagged by the
+``undersampling_correction`` step, and then ignored by the subsequent ``jump`` step. The smaller
+number of groups used for these pixels in the ``ramp_fitting`` step results in them having
 larger read noise variances, which in turn leads to lower weights used during resampling. This 
 ultimately leads to a lower than normal flux for the star in resampled images.
 
+Pixels that are the four nearest neighbors of those high-intensity pixels, will also be flagged
+as affected by charge migration.
+
 Once a group in a ramp has been flagged as affected by charge migration, all subsequent 
 groups in the ramp are also flagged. By flagging these groups, they are not used in the 
 computation of slopes in the :ref:`ramp_fitting <ramp_fitting_step>` step. However, as described 
@@ -41,6 +45,8 @@ This results in a readnoise variance for undersampled pixels that is similar to
 pixels unaffected by charge migration. For the Poisson noise variance calculation in 
 :ref:`ramp_fitting <ramp_fitting_step>`, the UNDERSAMP/DO_NOT_USE groups are not included.
 
+Pixels that are the four nearest neighbors of those high-intensity pixels will be similarly flagged.
+     
 For integrations having only 1 or 2 groups, no flagging will be performed.
 
 
diff --git a/jwst/assign_wcs/util.py b/jwst/assign_wcs/util.py
index f0e312c0b..50c8ba31d 100644
--- a/jwst/assign_wcs/util.py
+++ b/jwst/assign_wcs/util.py
@@ -958,6 +958,7 @@ def update_s_region_imaging(model):
 
     if bbox is None:
         bbox = wcs_bbox_from_shape(model.data.shape)
+        model.meta.wcs.bounding_box = bbox
 
     # footprint is an array of shape (2, 4) as we
     # are interested only in the footprint on the sky
diff --git a/jwst/pipeline/calwebb_detector1.py b/jwst/pipeline/calwebb_detector1.py
index 788d29c37..889e5f492 100644
--- a/jwst/pipeline/calwebb_detector1.py
+++ b/jwst/pipeline/calwebb_detector1.py
@@ -19,8 +19,8 @@ from ..linearity import linearity_step
 from ..dark_current import dark_current_step
 from ..reset import reset_step
 from ..persistence import persistence_step
+from ..undersampling_correction import undersampling_correction_step
 from ..jump import jump_step
-from ..undersampling_correction import  undersampling_correction_step
 from ..ramp_fitting import ramp_fit_step
 from ..gain_scale import gain_scale_step
 
@@ -60,8 +60,8 @@ class Detector1Pipeline(Pipeline):
                  'dark_current': dark_current_step.DarkCurrentStep,
                  'reset': reset_step.ResetStep,
                  'persistence': persistence_step.PersistenceStep,
-                 'jump': jump_step.JumpStep,
                  'undersampling_correction': undersampling_correction_step.UndersamplingCorrectionStep,
+                 'jump': jump_step.JumpStep,
                  'ramp_fit': ramp_fit_step.RampFitStep,
                  'gain_scale': gain_scale_step.GainScaleStep,
                  }
@@ -119,12 +119,12 @@ class Detector1Pipeline(Pipeline):
 
             input = self.dark_current(input)
 
-        # apply the jump step
-        input = self.jump(input)
-
         # apply the undersampling_correction step
         input = self.undersampling_correction(input)
 
+        # apply the jump step
+        input = self.jump(input)
+
         # save the corrected ramp data, if requested
         if self.save_calibrated_ramp:
             self.save_model(input, 'ramp')
diff --git a/jwst/undersampling_correction/undersampling_correction.py b/jwst/undersampling_correction/undersampling_correction.py
index f1b48513b..49c1a67ef 100644
--- a/jwst/undersampling_correction/undersampling_correction.py
+++ b/jwst/undersampling_correction/undersampling_correction.py
@@ -8,6 +8,11 @@ from stdatamodels.jwst.datamodels import dqflags
 log = logging.getLogger(__name__)
 log.setLevel(logging.DEBUG)
 
+GOOD = dqflags.group["GOOD"]
+DNU = dqflags.group["DO_NOT_USE"]
+UNSA = dqflags.group["UNDERSAMP"]
+UNSA_DNU = UNSA + DNU
+
 
 def undersampling_correction(input_model, signal_threshold):
     """
@@ -24,8 +29,8 @@ def undersampling_correction(input_model, signal_threshold):
     Returns
     -------
     output_model : `~jwst.datamodels.RampModel`
-        Data model with undersampling_correction applied; add UNDERSAMP flag
-        to groups exceeding signal_threshold
+        Data model with undersampling_correction applied; add UNDERSAMP and
+        DO_NOT_USE flags to groups exceeding signal_threshold
     """
     data = input_model.data
     gdq = input_model.groupdq
@@ -45,8 +50,8 @@ def undersampling_correction(input_model, signal_threshold):
 
 def flag_pixels(data, gdq, signal_threshold):
     """
-    Flag first group in each ramp that exceeds signal_threshold as UNDERSAMP and DO_NOT_USE,
-    skipping groups already flagged as DO_NOT_USE; then flag all subsequent groups in the ramp.
+    Flag each group in each ramp that exceeds signal_threshold as UNDERSAMP and DO_NOT_USE,
+    skipping groups already flagged as DO_NOT_USE.
 
     Parameters
     ----------
@@ -61,44 +66,78 @@ def flag_pixels(data, gdq, signal_threshold):
 
     Returns
     -------
-    gdq : int, 4D array
+    new_gdq : int, 4D array
         updated group dq array
     """
     n_ints, n_grps, n_rows, n_cols = gdq.shape
-    num_pix = n_cols * n_rows
+    ncols = data.shape[3]
+    nrows = data.shape[2]
+
+    new_gdq = gdq.copy()   # Updated gdq
+
+    # Flag all exceedances with UNDERSAMP and NO_NOT_USE
+    undersamp_pix = (data > signal_threshold) & (gdq != DNU)
+
+    new_gdq[undersamp_pix] = np.bitwise_or(new_gdq[undersamp_pix], UNSA | DNU)
+
+    # Reset groups previously flagged as DNU
+    gdq_orig = gdq.copy()  # For resetting to previously flagged DNU
+    wh_gdq_DNU = np.bitwise_and(gdq_orig, DNU)
+
+    # Get indices for exceedances
+    arg_where = np.argwhere(new_gdq == UNSA_DNU)
+    a_int = arg_where[:, 0]  # array of integrations
+    a_grp = arg_where[:, 1]  # array of groups
+    a_row = arg_where[:, 2]  # array of rows
+    a_col = arg_where[:, 3]  # array of columns
+
+    # Process the 4 nearest neighbors of each exceedance
+    # Pixel to the east
+    xx_max_p1 = a_col[a_col < (ncols-1)] + 1
+    i_int = a_int[a_col < (ncols-1)]
+    i_grp = a_grp[a_col < (ncols-1)]
+    i_row = a_row[a_col < (ncols-1)]
+
+    if len(xx_max_p1) > 0:
+        new_gdq[i_int, i_grp, i_row, xx_max_p1] = \
+            np.bitwise_or(new_gdq[i_int, i_grp, i_row, xx_max_p1], UNSA | DNU)
+
+    new_gdq[wh_gdq_DNU == 1] = gdq_orig[wh_gdq_DNU == 1]  # reset for earlier DNUs
+
+    # Pixel to the west
+    xx_m1 = a_col[a_col > 0] - 1
+    i_int = a_int[a_col > 0]
+    i_grp = a_grp[a_col > 0]
+    i_row = a_row[a_col > 0]
 
-    lowest_exc_1d = np.zeros(num_pix) + n_grps
+    if len(xx_m1) > 0:
+        new_gdq[i_int, i_grp, i_row, xx_m1] = \
+            np.bitwise_or(new_gdq[i_int, i_grp, i_row, xx_m1], UNSA | DNU)
 
-    for ii_int in range(n_ints):
-        for ii_grp in range(n_grps):
-            data_1d = data[ii_int, ii_grp, :, :].reshape(num_pix)  # vectorize slice
-            gdq_1d = gdq[ii_int, ii_grp, :, :].reshape(num_pix)
+    new_gdq[wh_gdq_DNU == 1] = gdq_orig[wh_gdq_DNU == 1]  # reset for earlier DNUs
 
-            wh_not_dnu = np.logical_not(gdq_1d & dqflags.group['DO_NOT_USE'])
+    # Pixel to the north
+    yy_m1 = a_row[a_row > 0] - 1
+    i_int = a_int[a_row > 0]
+    i_grp = a_grp[a_row > 0]
+    i_col = a_col[a_row > 0]
 
-            # In the current group for all ramps, locate pixels that :
-            #  a) exceed the signal_threshold, and
-            #  b) have not been previously flagged as an exceedance, and
-            #  c) were not flagged in an earlier step as DO_NOT_USE
-            wh_exc_1d = np.where((data_1d > signal_threshold) &
-                                 (lowest_exc_1d == n_grps) & wh_not_dnu)
+    if len(yy_m1) > 0:
+        new_gdq[i_int, i_grp, yy_m1, i_col] = \
+            np.bitwise_or(new_gdq[i_int, i_grp, yy_m1, i_col], UNSA | DNU)
 
-            # ... and mark those pixels, as current group is their first exceedance
-            if len(wh_exc_1d[0] > 0):  # For ramps previously unflagged ...
-                lowest_exc_1d[wh_exc_1d] = ii_grp
+    new_gdq[wh_gdq_DNU == 1] = gdq_orig[wh_gdq_DNU == 1]  # reset for earlier DNUs
 
-    # Flag current and subsequent groups
-    lowest_exc_2d = lowest_exc_1d.reshape((n_rows, n_cols))
-    for ii_int in range(n_ints):
-        for ii_grp in range(n_grps):
-            wh_set_flag = np.where(lowest_exc_2d == ii_grp)
+    # Pixel to the south
+    yy_max_p1 = a_row[a_row < (nrows-1)] + 1
+    i_int = a_int[a_row < (nrows-1)]
+    i_grp = a_grp[a_row < (nrows-1)]
+    i_col = a_col[a_row < (nrows-1)]
 
-            # set arrays of components
-            yy = wh_set_flag[0]
-            xx = wh_set_flag[1]
+    if len(yy_max_p1) > 0:
+        new_gdq[i_int, i_grp, yy_max_p1, i_col] = \
+            np.bitwise_or(new_gdq[i_int, i_grp, yy_max_p1, i_col], UNSA | DNU)
 
-            gdq[ii_int, ii_grp:, yy, xx] = \
-                np.bitwise_or(gdq[ii_int, ii_grp:, yy, xx], dqflags.group['UNDERSAMP']
-                              | dqflags.group['DO_NOT_USE'])
+    new_gdq[wh_gdq_DNU == 1] = gdq_orig[wh_gdq_DNU == 1]  # reset for earlier DNUs
 
-    return gdq
+    return new_gdq
diff --git a/jwst/undersampling_correction/undersampling_correction_step.py b/jwst/undersampling_correction/undersampling_correction_step.py
index e5b8ba6b0..190846058 100755
--- a/jwst/undersampling_correction/undersampling_correction_step.py
+++ b/jwst/undersampling_correction/undersampling_correction_step.py
@@ -21,7 +21,7 @@ class UndersamplingCorrectionStep(Step):
     class_alias = "undersampling_correction"
 
     spec = """
-        signal_threshold = float(default=30000)
+        signal_threshold = float(default=25000)
         skip = boolean(default=True)
     """
 
@@ -31,7 +31,7 @@ class UndersamplingCorrectionStep(Step):
         with datamodels.RampModel(input) as input_model:
             if (input_model.data.shape[1] < 3):  # skip step if only 1 or 2 groups/integration
                 log.info('Too few groups per integration; skipping undersampling_correction')
-                
+
                 result = input_model
                 result.meta.cal_step.undersampling_correction = 'SKIPPED'
 


Test patch:
diff --git a/jwst/regtest/test_associations_sdp_pools.py b/jwst/regtest/test_associations_sdp_pools.py
index de7e20033..7f29fac43 100644
--- a/jwst/regtest/test_associations_sdp_pools.py
+++ b/jwst/regtest/test_associations_sdp_pools.py
@@ -32,11 +32,6 @@ SPECIAL_DEFAULT = {
     'slow': False,
 }
 SPECIAL_POOLS = {
-    'jw00016_20230331t130733_pool': {
-        'args': [],
-        'xfail': 'See issue JP-3230',
-        'slow': False,
-    },
     'jw00623_20190607t021101_pool': {
         'args': [],
         'xfail': None,
diff --git a/jwst/undersampling_correction/tests/test_undersampling_correction.py b/jwst/undersampling_correction/tests/test_undersampling_correction.py
index 12b90e751..806a47a3b 100644
--- a/jwst/undersampling_correction/tests/test_undersampling_correction.py
+++ b/jwst/undersampling_correction/tests/test_undersampling_correction.py
@@ -12,8 +12,8 @@ test_dq_flags = dqflags.pixel
 GOOD = test_dq_flags["GOOD"]
 DNU = test_dq_flags["DO_NOT_USE"]
 UNSA = test_dq_flags["UNDERSAMP"]
-ADFL = test_dq_flags["AD_FLOOR"]
 DROU = test_dq_flags["DROPOUT"]
+UNSA_DNU = UNSA + DNU
 
 
 def test_pix_0():
@@ -21,7 +21,7 @@ def test_pix_0():
     Having all data in ramp below the signal threshold, the only non-GOOD
     groups in the output GROUPDQ should be those DNU propagated from the input.
     """
-    ngroups, nints, nrows, ncols = set_scalars()
+    ngroups, nints, nrows, ncols = 10, 1, 1, 1
     ramp_model, pixdq, groupdq, err = create_mod_arrays(
         ngroups, nints, nrows, ncols)
 
@@ -42,59 +42,35 @@ def test_pix_0():
 
 def test_pix_1():
     """
-    All input GROUPDQ = 'GOOD'. Some ramp data exceed the signal threshold, so the
-    only non-GOOD groups in the output GROUPDQ should be UNSA + DNU for the first
-    group exceeding the signal threshold and all subsequent groups.
+    Tests groups whose data exceeds the signal threshold; 1 group is already
+    flagged as DNU from a previous calibration step, and 1 group is GOOD.
+    Also tests groups whose data does not exceed the signal threshold;
+    similarly 1 group is already flagged as DNU from a previous calibration
+    step, and 1 is GOOD. All data beyond the first exceedance are also
+    flagged as UNDERSAMP and DNU.
     """
-    ngroups, nints, nrows, ncols = set_scalars()
+    ngroups, nints, nrows, ncols = 10, 1, 1, 1
     ramp_model, pixdq, groupdq, err = create_mod_arrays(
         ngroups, nints, nrows, ncols)
 
     signal_threshold = 20000.
 
-    # Populate pixel-specific SCI and GROUPDQ arrays
-    # Set seom groups' SCI to be above the signal threshold, and all input
-    # GROUPDQ to be GOOD
-    ramp_model.data[0, 3, 0, 0] = np.array((signal_threshold + 100.), dtype=np.float32)
-    ramp_model.data[0, 5, 0, 0] = np.array((signal_threshold - 200.), dtype=np.float32)
-    ramp_model.data[0, 8, 0, 0] = np.array((signal_threshold + 100.), dtype=np.float32)
-    ramp_model.groupdq[0, :, 0, 0] = [GOOD] * ngroups
-
-    true_out_gdq = ramp_model.groupdq.copy()  # all GOOD
-    true_out_gdq[0, :, 0, 0] = [GOOD] * ngroups
-    true_out_gdq[0, 3:, 0, 0] = np.bitwise_or(UNSA, DNU)
-
-    out_model = undersampling_correction(ramp_model, signal_threshold)
-    out_gdq = out_model.groupdq
-
-    npt.assert_array_equal(out_gdq, true_out_gdq)
-
-
-def test_pix_2():
-    """
-    Tests groups having data exceeding the signal threshold, Some groups are
-    already flagged as DO_NOT_USE; they will not be checked for UC,  Other
-    groups will have 'DNU'+'UNSA' added to their GROUPDQ, as will all later
-    groups.
-    """
-    ngroups, nints, nrows, ncols = set_scalars()
-    ramp_model, pixdq, groupdq, err = create_mod_arrays(
-        ngroups, nints, nrows, ncols)
+    # Populate SCI and GROUPDQ arrays.
+    ramp_model.data[0, 1, 0, 0] = np.array((0.5 * signal_threshold), dtype=np.float32)
 
-    signal_threshold = 20000.
+    ramp_model.data[0, 2, 0, 0] = np.array((0.8 * signal_threshold), dtype=np.float32)
+    ramp_model.groupdq[0, 2, 0, 0] = DNU  # should not get UNSA, not an exceedance
 
-    # Populate SCI and GROUPDQ arrays.
-    ramp_model.data[0, 1, 0, 0] = np.array((signal_threshold + 100.), dtype=np.float32)
-    ramp_model.data[0, 2, 0, 0] = np.array((signal_threshold + 100.), dtype=np.float32)
-    ramp_model.data[0, 3, 0, 0] = np.array((signal_threshold + 100.), dtype=np.float32)
+    ramp_model.data[0, 3, 0, 0] = np.array((signal_threshold + 5000.), dtype=np.float32)
+    ramp_model.groupdq[0, 3, 0, 0] = DNU  # should not get UNSA, although exceedance
 
-    ramp_model.groupdq[0, 1, 0, 0] = DNU  # should not get UNSA
-    ramp_model.groupdq[0, 2, 0, 0] = np.bitwise_or(ADFL, DNU)  # should not get UNSA
-    ramp_model.groupdq[0, 3, 0, 0] = ADFL  # should get UNSA + DNU
+    ramp_model.data[0, 4:, 0, 0] = np.array((signal_threshold + 6000.), dtype=np.float32)
+    ramp_model.groupdq[0, 4:, 0, 0] = GOOD
 
     true_out_gdq = ramp_model.groupdq.copy()
-    true_out_gdq[0, 3, 0, 0] = np.bitwise_or(np.bitwise_or(DNU, UNSA), ADFL)
-    true_out_gdq[0, 4:, 0, 0] = np.bitwise_or(DNU, UNSA)
+    true_out_gdq[0, 2, 0, 0] = DNU
+    true_out_gdq[0, 3, 0, 0] = DNU
+    true_out_gdq[0, 4:, 0, 0] = UNSA_DNU
 
     out_model = undersampling_correction(ramp_model, signal_threshold)
     out_gdq = out_model.groupdq
@@ -107,9 +83,7 @@ def test_too_few_groups():
     Test that processing for datasets having too few (<3) groups per integration
     are skipped.
     """
-    ngroups, nints, nrows, ncols = set_scalars()
-    ngroups = 2
-
+    ngroups, nints, nrows, ncols = 2, 1, 1, 1
     ramp_model, pixdq, groupdq, err = create_mod_arrays(
         ngroups, nints, nrows, ncols)
 
@@ -123,16 +97,92 @@ def test_too_few_groups():
     npt.assert_string_equal(status, "SKIPPED")
 
 
-def set_scalars():
+def test_flag_neighbors():
     """
-    Set needed scalars for the size of the dataset,
+    Test flagging of 4 nearest neighbors of exceedances. Tests pixels on
+    array edges, Tests exclusion of groups previously flagged as DO_NOT_USE.
     """
-    ngroups = 10
-    nints = 1
-    nrows = 1
-    ncols = 1
+    ngroups, nints, nrows, ncols = 6, 1, 4, 3
+    ramp_model, pixdq, groupdq, err = create_mod_arrays(
+        ngroups, nints, nrows, ncols)
+
+    signal_threshold = 4400.
+
+    # Populate pixel-specific SCI and GROUPDQ arrays
+    ramp_model.data[0, :, :, :] = \
+        np.array([[
+            [1900., 2666., 2100.],
+            [3865., 2300., 3177.],
+            [3832., 3044., 3588.],
+            [3799., 3233., 3000.]],
+
+           [[2100., 2866., 2300.],
+            [4065., 2500., 3377.],
+            [4032., 3244., 3788.],
+            [3999., 3433., 3200.]],
+
+           [[2300., 3066., 2500.],
+            [4265., 2700., 3577.],
+            [4232., 3444., 3988.],
+            [4199., 3633., 3400.]],
+
+           [[2500., 3266., 2700.],
+            [4465., 2900., 3777.],
+            [4432., 3644., 4188.],
+            [4399., 3833., 3600.]],
+
+           [[2700., 3466., 2900.],
+            [4665., 3100., 3977.],
+            [4632., 3844., 4388.],
+            [4599., 4033., 3800.]],
+
+           [[2900., 3666., 3100.],
+            [4865., 3300., 4177.],
+            [4832., 4044., 4588.],
+            [4799., 4233., 4000.]]], dtype=np.float32)
+
+    # These group DQ values should propagate unchanged to the output
+    ramp_model.groupdq[:, 4, 2, 0] = [DNU]
+    ramp_model.groupdq[:, 1, 2, 2] = [DNU]
+    ramp_model.groupdq[:, 2, 1, 1] = [DROU + DNU]
+
+    out_model = undersampling_correction(ramp_model, signal_threshold)
+    out_gdq = out_model.groupdq
 
-    return ngroups, nints, nrows, ncols
+    true_out_gdq = ramp_model.groupdq.copy()
+    true_out_gdq[0, :, :, :] = \
+        np.array([[
+            [0,   0,   0],
+            [0,   0,   0],
+            [0,   0,   0],
+            [0,   0,   0]],
+
+           [[0,   0,   0],
+            [0,   0,   0],
+            [0,   0,   DNU],
+            [0,   0,   0]],
+
+           [[0,   0,   0],
+            [0,   9,   0],
+            [0,   0,   0],
+            [0,   0,   0]],
+
+           [[UNSA_DNU,   0,      0],
+            [UNSA_DNU, UNSA_DNU, 0],
+            [UNSA_DNU, UNSA_DNU, 0],
+            [UNSA_DNU,   0,      0]],
+
+           [[UNSA_DNU,   0,      0],
+            [UNSA_DNU, UNSA_DNU, 0],
+            [DNU,        0,      0],
+            [UNSA_DNU, UNSA_DNU, 0]],
+
+           [[UNSA_DNU,   0,       0],
+            [UNSA_DNU, UNSA_DNU, UNSA_DNU],
+            [UNSA_DNU, UNSA_DNU, UNSA_DNU],
+            [UNSA_DNU, UNSA_DNU, UNSA_DNU]]], dtype=np.uint8)
+
+    npt.assert_array_equal(out_gdq, true_out_gdq)
 
 
 def create_mod_arrays(ngroups, nints, nrows, ncols):