Repository: spacetelescope/jwst
Pull request #9356: JP-3956: Fix error propagation for inverse steps (photom, flatfield)
Author: login: pllim
Labels: name: bug
name: flatfield
name: photom
name: testing
name: documentation
Created: 2025-04-03T01:35:35Z | Merged: 2025-04-16T14:15:27Z

<!-- 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 [JP-3956](https://jira.stsci.edu/browse/JP-3956)

<!-- If this PR closes a GitHub issue that is not attached to a Jira ticket, uncomment the line below, and reference it here by its number -->
<!-- Closes # -->

<!-- describe the changes comprising this PR here -->
This PR addresses:

* BUG: Fix err and var for inverse steps.
* STY: Minor docstring and style fixes.

Also see:

* #9330
* https://github.com/spacetelescope/jwst/pull/9085
* https://github.com/spacetelescope/jwst/pull/9254
* https://github.com/spacetelescope/jwst/pull/9320

<!-- if you can't perform these tasks due to permissions, please ask a maintainer to do them -->
## Tasks
- [x] If you have a specific reviewer in mind, tag them.
- [x] add a build milestone, i.e. `Build 12.0` (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`)
  - [x] write news fragment(s) in `changes/`: `echo "changed something" > changes/<PR#>.<changetype>.rst` (see [changelog readme](https://github.com/spacetelescope/jwst/blob/main/changes/README.rst) for instructions) 
  - [ ] update or add relevant tests
  - [x] update relevant docstrings and / or `docs/` page
  - [x] [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)) https://github.com/spacetelescope/RegressionTests/actions/runs/14477166563 and https://github.com/spacetelescope/RegressionTests/actions/runs/14479420182
    - [ ] 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)



Diff:
diff --git a/changes/9356.flatfield.rst b/changes/9356.flatfield.rst
new file mode 100644
index 00000000000..33a3dcc7ea1
--- /dev/null
+++ b/changes/9356.flatfield.rst
@@ -0,0 +1 @@
+Fixed error and variance propagation for inverse steps.
diff --git a/changes/9356.photom.rst b/changes/9356.photom.rst
new file mode 100644
index 00000000000..33a3dcc7ea1
--- /dev/null
+++ b/changes/9356.photom.rst
@@ -0,0 +1 @@
+Fixed error and variance propagation for inverse steps.
diff --git a/docs/jwst/flatfield/arguments.rst b/docs/jwst/flatfield/arguments.rst
index 52a61bcd3f2..5ba0077cd6e 100644
--- a/docs/jwst/flatfield/arguments.rst
+++ b/docs/jwst/flatfield/arguments.rst
@@ -14,5 +14,5 @@ the behavior of the processing.
 
 ``--inverse`` (boolean, default=False)
   A flag to indicate whether the math operations used to apply the
-  flat-field should be inverted (i.e. multiply the flat-field into
-  the science data, instead of the usual division).
+  flat-field should be inverted (i.e., multiply the flat-field into
+  the science, error, and variance data, instead of the usual division).
diff --git a/docs/jwst/photom/arguments.rst b/docs/jwst/photom/arguments.rst
index c7893ca3694..052cbdd0cf4 100644
--- a/docs/jwst/photom/arguments.rst
+++ b/docs/jwst/photom/arguments.rst
@@ -4,8 +4,8 @@ The ``photom`` step has the following optional arguments.
 
 ``--inverse`` (boolean, default=False)
   A flag to indicate whether the math operations used to apply the
-  correction should be inverted (i.e. divide the calibration data
-  into the science data, instead of the usual multiplication).
+  correction should be inverted (i.e., divide the calibration data
+  into the science, error, and variance data, instead of the usual multiplication).
 
 ``--source_type`` (string, default=None)
   Force the processing to use the given source type (POINT, EXTENDED),
@@ -13,5 +13,4 @@ The ``photom`` step has the following optional arguments.
 
 ``--mrs_time_correction`` (boolean, default=True)
    A flag to indicate whether to turn on the time and wavelength dependent
-   correction for MIRI MRS data. 
-   
+   correction for MIRI MRS data.
diff --git a/jwst/flatfield/flat_field.py b/jwst/flatfield/flat_field.py
index c4e800885c9..4380a614973 100644
--- a/jwst/flatfield/flat_field.py
+++ b/jwst/flatfield/flat_field.py
@@ -31,7 +31,8 @@
 def do_correction(input_model,
                   flat=None, fflat=None, sflat=None, dflat=None, user_supplied_flat=None,
                   inverse=False):
-    """Flat-field a JWST data model using a flat-field model
+    """
+    Flat-field a JWST data model using a flat-field model.
 
     Parameters
     ----------
@@ -55,7 +56,7 @@ def do_correction(input_model,
         If supplied, all other reference flats and flat creation are
         ignored in favor of the specified flat.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
 
     Returns
@@ -98,17 +99,18 @@ def do_correction(input_model,
 #
 
 def do_flat_field(output_model, flat_model, inverse=False):
-    """Apply flat-fielding for non-NIRSpec modes, updating the output model.
+    """
+    Apply flat-fielding for non-NIRSpec modes, updating the output model.
 
     Parameters
     ----------
     output_model : JWST data model
-        flat-fielded input science data model, modified in-place
+        Flat-fielded input science data model, modified in-place.
 
     flat_model : JWST data model
-        data model containing flat-field
+        Data model containing flat-field.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
     """
     if output_model.meta.instrument.name == "NIRSPEC":
@@ -145,10 +147,9 @@ def do_flat_field(output_model, flat_model, inverse=False):
 
 
 def apply_flat_field(science, flat, inverse=False):
-    """Flat field the data and error arrays.
+    """
+    Flat field the data and error arrays.
 
-    Extended summary
-    ----------------
     The science data and error arrays will be divided by the flat field.
     The data quality array will be updated based on bad pixels in flat
     field arrays. Applies portion of flat field corresponding to science
@@ -157,12 +158,12 @@ def apply_flat_field(science, flat, inverse=False):
     Parameters
     ----------
     science : JWST data model
-        input science data model
+        Input science data model.
 
     flat : JWST data model
-        flat field data model
+        Flat field data model.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
     """
 
@@ -205,23 +206,28 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        # NOTE: Re-arranging var_flat math will cause some NIRISS SOSS
+        # regression tests to fail with floating point differences.
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
     else:
         science.data *= flat_data
+        science.var_flat = np.zeros_like(science.data)  # Does not exist before flatfield step
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
-        science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
-    else:
-        flat_data_squared = flat_data ** 2
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
-
+        if not inverse:
+            science.var_poisson /= flat_data_squared
+            science.var_rnoise /= flat_data_squared
+            science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
+        else:
+            science.var_poisson *= flat_data_squared
+            science.var_rnoise *= flat_data_squared
+            science.err = np.sqrt(science.var_poisson + science.var_rnoise)
+    elif not inverse:
         # Set the output ERR to be the combined input ERR plus flatfield ERR, summed in quadrature
         science.err = np.sqrt(science.err**2 + science.var_flat)
 
@@ -239,10 +245,14 @@ def apply_flat_field(science, flat, inverse=False):
 
 def do_nirspec_flat_field(output_model, f_flat_model, s_flat_model, d_flat_model,
                           user_supplied_flat=None, inverse=False):
-    """Apply flat-fielding for NIRSpec data, updating in-place.
+    """
+    Apply flat-fielding for NIRSpec data, updating in-place.
 
-    Calls one of 3 functions depending on whether the data is 1) NIRSpec IFU,
-    2) NIRSpec BRIGHTOBJ or 3) NIRSpec MSA or Fixed-slit.
+    Calls one of 3 functions depending on whether the data is:
+
+    1. NIRSpec IFU,
+    2. NIRSpec BRIGHTOBJ, or
+    3. NIRSpec MSA or Fixed-slit.
 
     Parameters
     ----------
@@ -262,12 +272,12 @@ def do_nirspec_flat_field(output_model, f_flat_model, s_flat_model, d_flat_model
         If provided, override all other calculated or reference-file-retrieved
         flat information and use this data.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
 
     Returns
     -------
-    ~jwst.datamodels.MultiSlitModel or ~jwst.datamodels.ImageModel
+    result : `~jwst.datamodels.MultiSlitModel` or `~jwst.datamodels.ImageModel`
         The interpolated flat field(s).
     """
 
@@ -318,7 +328,8 @@ def do_nirspec_flat_field(output_model, f_flat_model, s_flat_model, d_flat_model
 
 def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis,
                    user_supplied_flat=None, inverse=False):
-    """Apply flat-fielding for NIRSpec fixed slit and MSA data, in-place
+    """
+    Apply flat-fielding for NIRSpec fixed slit and MSA data, in-place.
 
     Parameters
     ----------
@@ -341,12 +352,12 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
         If provided, override all other calculated or reference-file-retrieved
         flat information and use this data.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
 
     Returns
     -------
-    interpolated_flats: `~jwst.datamodels.MultiSlitModel`
+    interpolated_flats : `~jwst.datamodels.MultiSlitModel`
         The interpolated flat field, one for each slit.
     """
 
@@ -419,18 +430,21 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.err = np.sqrt(slit.var_poisson + slit.var_rnoise + slit.var_flat)
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
-        slit.err = np.sqrt(slit.var_poisson + slit.var_rnoise + slit.var_flat)
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = np.zeros_like(slit.data)  # Does not exist before flatfield step
+            slit.err = np.sqrt(slit.var_poisson + slit.var_rnoise)
 
         # Combine the science and flat DQ arrays
         slit.dq |= slit_flat.dq
@@ -458,35 +472,36 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
 
 def nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis,
                       user_supplied_flat=None, inverse=False):
-    """Apply flat-fielding for NIRSpec BRIGHTOBJ data, in-place
+    """
+    Apply flat-fielding for NIRSpec BRIGHTOBJ data, in-place.
 
     Parameters
     ----------
     output_model : JWST data model
         CubeModel, modified (flat fielded) plane by plane, in-place.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    f_flat_model :` ~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     dispaxis : int
         1 means horizontal dispersion, 2 means vertical dispersion.
 
-    user_supplied_flat : ~jwst.datamodels.ImageModel or None
+    user_supplied_flat : `~jwst.datamodels.ImageModel` or None
         A pre-computed flat to use directly. If supplied,
-        all other inputs are ignored
+        all other inputs are ignored.
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
 
     Returns
     -------
-    ~jwst.datamodels.ImageModel
+    result : `~jwst.datamodels.ImageModel`
         The interpolated flat field.
     """
 
@@ -498,21 +513,26 @@ def nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_model, di
             output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis
         )
 
+    # Update the variances and uncertainty array using BASELINE algorithm
+    flat_data_squared = interpolated_flat.data * interpolated_flat.data
     if not inverse:
         output_model.data /= interpolated_flat.data
+        output_model.var_poisson /= flat_data_squared
+        output_model.var_rnoise /= flat_data_squared
+        output_model.var_flat = (output_model.data / interpolated_flat.data * interpolated_flat.err) ** 2
+        output_model.err = np.sqrt(
+            output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
+        )
     else:
         output_model.data *= interpolated_flat.data
+        output_model.var_poisson *= flat_data_squared
+        output_model.var_rnoise *= flat_data_squared
+        output_model.var_flat = np.zeros_like(output_model.data)  # Does not exist before flatfield step
+        output_model.err = np.sqrt(
+            output_model.var_poisson + output_model.var_rnoise
+        )
     output_model.dq |= interpolated_flat.dq
 
-    # Update the variances and uncertainty array using BASELINE algorithm
-    flat_data_squared = interpolated_flat.data ** 2
-    output_model.var_poisson /= flat_data_squared
-    output_model.var_rnoise /= flat_data_squared
-    output_model.var_flat = (output_model.data / interpolated_flat.data * interpolated_flat.err) ** 2
-    output_model.err = np.sqrt(
-        output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
-    )
-
     # Make sure all NaNs and flags match up in the output model
     pipe_utils.match_nans_and_flags(output_model)
 
@@ -523,35 +543,36 @@ def nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_model, di
 
 def nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis,
                 user_supplied_flat=None, inverse=False):
-    """Apply flat-fielding for NIRSpec IFU data, in-place
+    """
+    Apply flat-fielding for NIRSpec IFU data, in-place.
 
     Parameters
     ----------
     output_model : JWST data model
         Science data model, modified (flat fielded) in-place.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     dispaxis : int
         1 means horizontal dispersion, 2 means vertical dispersion.
 
-    user_supplied_flat : ~jwst.datamodels.ImageModel or None
+    user_supplied_flat : `~jwst.datamodels.ImageModel` or None
         A pre-computed flat to use directly. If supplied,
         all other inputs are ignored
 
-    inverse : boolean
+    inverse : bool
         Invert the math operations used to apply the flat field.
 
     Returns
     -------
-    ~jwst.datamodels.ImageModel
+    result : `~jwst.datamodels.ImageModel`
         The interpolated flat field.
     """
 
@@ -567,21 +588,26 @@ def nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis
         )
 
     if any_updated:
+        # Update the variances and uncertainty array using BASELINE algorithm
+        flat_data_squared = flat * flat
         if not inverse:
             output_model.data /= flat
+            output_model.var_poisson /= flat_data_squared
+            output_model.var_rnoise /= flat_data_squared
+            output_model.var_flat = (output_model.data / flat * flat_err) ** 2
+            output_model.err = np.sqrt(
+                output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
+            )
         else:
             output_model.data *= flat
+            output_model.var_poisson *= flat_data_squared
+            output_model.var_rnoise *= flat_data_squared
+            output_model.var_flat = np.zeros_like(output_model.data)  # Does not exist before flatfield step
+            output_model.err = np.sqrt(
+                output_model.var_poisson + output_model.var_rnoise
+            )
         output_model.dq |= flat_dq
 
-        # Update the variances and uncertainty array using BASELINE algorithm
-        flat_data_squared = flat ** 2
-        output_model.var_poisson /= flat_data_squared
-        output_model.var_rnoise /= flat_data_squared
-        output_model.var_flat = (output_model.data / flat * flat_err) ** 2
-        output_model.err = np.sqrt(
-            output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
-        )
-
         # Make sure all NaNs and flags match up in the output model
         pipe_utils.match_nans_and_flags(output_model)
 
@@ -600,7 +626,8 @@ def nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis
 def create_flat_field(wl, f_flat_model, s_flat_model, d_flat_model,
                       xstart, xstop, ystart, ystop,
                       exposure_type, dispaxis, slit_name, slit_nt=None):
-    """Extract and combine flat field components for NIRSpec
+    """
+    Extract and combine flat field components for NIRSpec.
 
     Parameters
     ----------
@@ -608,13 +635,13 @@ def create_flat_field(wl, f_flat_model, s_flat_model, d_flat_model,
         Wavelength at each pixel of the 2-D slit array.  This array has
         shape (ystop - ystart, xstop - xstart).
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     xstart, ystart : int
@@ -624,7 +651,7 @@ def create_flat_field(wl, f_flat_model, s_flat_model, d_flat_model,
     xstop, ystop : int
         End of the slice containing the data for the current slit.  The
         start and stop values are Python slice notation, i.e. the region
-        to be extracted is [ystart:ystop, xstart:xstop].
+        to be extracted is ``[ystart:ystop, xstart:xstop]``.
 
     exposure_type : str
         The exposure type refers to fixed_slit, IFU, or using the
@@ -691,14 +718,15 @@ def create_flat_field(wl, f_flat_model, s_flat_model, d_flat_model,
 
 def fore_optics_flat(wl, f_flat_model, exposure_type, dispaxis,
                      slit_name, slit_nt):
-    """Extract the flat for the fore optics part.
+    """
+    Extract the flat for the fore optics part.
 
     Parameters
     ----------
     wl : 2-D ndarray
         Wavelength at each pixel of the 2-D slit array.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
     exposure_type : str
@@ -810,14 +838,15 @@ def fore_optics_flat(wl, f_flat_model, exposure_type, dispaxis,
 def spectrograph_flat(wl, s_flat_model,
                       xstart, xstop, ystart, ystop,
                       exposure_type, dispaxis, slit_name):
-    """Extract the flat for the spectrograph part.
+    """
+    Extract the flat for the spectrograph part.
 
     Parameters
     ----------
     wl : 2-D ndarray
         Wavelength at each pixel of the 2-D slit array.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
     xstart, ystart : int
@@ -827,7 +856,7 @@ def spectrograph_flat(wl, s_flat_model,
     xstop, ystop : int
         End of the slice containing the data for the current slit.  The
         start and stop values are Python slice notation, i.e. the region
-        to be extracted is [ystart:ystop, xstart:xstop].
+        to be extracted is ``[ystart:ystop, xstart:xstop]``.
 
     exposure_type : str
         The exposure type refers to fixed_slit, IFU, or using the
@@ -918,14 +947,15 @@ def spectrograph_flat(wl, s_flat_model,
 def detector_flat(wl, d_flat_model,
                   xstart, xstop, ystart, ystop,
                   exposure_type, dispaxis, slit_name):
-    """Extract the flat for the detector part.
+    """
+    Extract the flat for the detector part.
 
     Parameters
     ----------
     wl : 2-D ndarray
         Wavelength at each pixel of the 2-D slit array.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     xstart, ystart : int
@@ -935,7 +965,7 @@ def detector_flat(wl, d_flat_model,
     xstop, ystop : int
         End of the slice containing the data for the current slit.  The
         start and stop values are Python slice notation, i.e. the region
-        to be extracted is [ystart:ystop, xstart:xstop].
+        to be extracted is ``[ystart:ystop, xstart:xstop]``.
 
     exposure_type : str
         The exposure type refers to fixed_slit, IFU, or using the
@@ -1015,7 +1045,8 @@ def detector_flat(wl, d_flat_model,
 
 
 def combine_dq(f_flat_dq, s_flat_dq, d_flat_dq, default_shape):
-    """Combine non-None DQ arrays via bitwise_or.
+    """
+    Combine non-None DQ arrays via bitwise_or.
 
     Parameters
     ----------
@@ -1072,17 +1103,16 @@ def combine_dq(f_flat_dq, s_flat_dq, d_flat_dq, default_shape):
 
 
 def read_image_wl(flat_model, quadrant=None):
-    """Read wavelengths for the image planes.
+    """
+    Read wavelengths for the image planes.
 
-    Extended summary
-    ----------------
     This function should only be called if the SCI array in `flat_model`
     is 3-D.  The purpose is to get the wavelength for each of the planes
     in the SCI array.
 
     Parameters
     ----------
-    flat_model : ~jwst.datamodels.NirspecFlatModel or ~jwst.datamodels.NirspecQuadFlatModel
+    flat_model : `~jwst.datamodels.NirspecFlatModel` or `~jwst.datamodels.NirspecQuadFlatModel`
         Flat field for the current component.
 
     quadrant : int (0, 1, 2, or 3)
@@ -1125,7 +1155,8 @@ def read_image_wl(flat_model, quadrant=None):
 
 
 def read_flat_table(flat_model, exposure_type, slit_name=None, quadrant=None):
-    """Read the table (the "fast" variation).
+    """
+    Read the table (the "fast" variation).
 
     Parameters
     ----------
@@ -1272,7 +1303,8 @@ def read_flat_table(flat_model, exposure_type, slit_name=None, quadrant=None):
 
 
 def combine_fast_slow(wl, flat_2d, flat_dq, flat_err, tab_wl, tab_flat, tab_flat_error, dispaxis):
-    """Multiply the image by the tabular values.
+    """
+    Multiply the image by the tabular values.
 
     Parameters
     ----------
@@ -1392,7 +1424,8 @@ def combine_fast_slow(wl, flat_2d, flat_dq, flat_err, tab_wl, tab_flat, tab_flat
 
 
 def clean_wl(wl, dispaxis):
-    """Replace zeros and/or NaNs in the wavelength array.
+    """
+    Replace zeros and/or NaNs in the wavelength array.
 
     Parameters
     ----------
@@ -1405,7 +1438,7 @@ def clean_wl(wl, dispaxis):
     Returns
     -------
     wl_c : 2-D array
-        A copy of `wl`, but with zero and negative values replaced with
+        A copy of ``wl``, but with zero and negative values replaced with
         an average wavelength.  For each column (row) in the dispersion
         direction, the average to find a replacement value is taken along
         the cross-dispersion direction.
@@ -1456,7 +1489,8 @@ def clean_wl(wl, dispaxis):
 
 
 def interpolate_flat(image_flat, image_dq, image_err, image_wl, wl):
-    """Interpolate within the 3-D flat field image to get a 2-D flat.
+    """
+    Interpolate within the 3-D flat field image to get a 2-D flat.
 
     Parameters
     ----------
@@ -1483,15 +1517,15 @@ def interpolate_flat(image_flat, image_dq, image_err, image_wl, wl):
     Returns
     -------
     flat_2d : ndarray, 2-D, float32
-        The flat field, interpolated over wavelength, same shape as `wl`.
+        The flat field, interpolated over wavelength, same shape as ``wl``.
         Divide the 2-D extracted spectrum by this array to correct for
         flat-field variations.
 
     flat_dq : ndarray, 2-D, uint32
-        The data quality array corresponding to `flat_2d`.
+        The data quality array corresponding to ``flat_2d``.
 
     flat_err : ndarray, 2-D, float32
-        The error array corresponding to `flat_2d`.
+        The error array corresponding to ``flat_2d``.
     """
 
     if len(image_flat.shape) < 3:
@@ -1592,20 +1626,21 @@ def interpolate_flat(image_flat, image_dq, image_err, image_wl, wl):
 
 def flat_for_nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model,
                          dispaxis):
-    """Create the interpolated flat for NIRSpec IFU
+    """
+    Create the interpolated flat for NIRSpec IFU.
 
     Parameters
     ----------
     output_model : JWST data model
         Science data model, modified (flat fielded) in-place.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     dispaxis : int
@@ -1734,20 +1769,21 @@ def flat_for_nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model,
 
 def flat_for_nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_model,
                                dispaxis):
-    """Create the interpolated flat for NIRSpec IFU
+    """
+    Create the interpolated flat for NIRSpec IFU.
 
     Parameters
     ----------
     output_model : JWST data model
         Science data model, modified (flat fielded) in-place.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     dispaxis : int
@@ -1849,20 +1885,21 @@ def flat_for_nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_
 def flat_for_nirspec_slit(slit, f_flat_model, s_flat_model, d_flat_model,
                           dispaxis, exposure_type, slit_nt, subarray,
                           use_wavecorr):
-    """Create the interpolated flat for NIRSpec slit data
+    """
+    Create the interpolated flat for NIRSpec slit data.
 
     Parameters
     ----------
     slit : SlitModel
-        A slit to process
+        A slit to process.
 
-    f_flat_model : ~jwst.datamodels.NirspecFlatModel, ~jwst.datamodels.NirspecQuadFlatModel, or None
+    f_flat_model : `~jwst.datamodels.NirspecFlatModel`, `~jwst.datamodels.NirspecQuadFlatModel`, or None
         Flat field for the fore optics.
 
-    s_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    s_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the spectrograph.
 
-    d_flat_model : ~jwst.datamodels.NirspecFlatModel or None
+    d_flat_model : `~jwst.datamodels.NirspecFlatModel` or None
         Flat field for the detector.
 
     dispaxis : int
@@ -1876,16 +1913,16 @@ def flat_for_nirspec_slit(slit, f_flat_model, s_flat_model, d_flat_model,
         the indices of the current shutter in the Y and X directions.
 
     subarray : DataModel.meta.subarray
-        The subarray specification
+        The subarray specification.
 
-    use_wavecorr : boolean
+    use_wavecorr : bool
         Flag indicating whether or not to use the corrected wavelengths
         provided (upstream) by the wavecorr step.
 
     Returns
     -------
     flat : SlitModel or None
-        The calculated flat. If None, no flat field could be determined
+        The calculated flat. If None, no flat field could be determined.
     """
     # Create flat and flat dq arrays with default values
     flat_2d = np.ones_like(slit.data)
diff --git a/jwst/flatfield/tests/test_clean_wl.py b/jwst/flatfield/tests/test_clean_wl.py
index a074ebe5588..d4842f81821 100644
--- a/jwst/flatfield/tests/test_clean_wl.py
+++ b/jwst/flatfield/tests/test_clean_wl.py
@@ -2,6 +2,7 @@
 Test for flat_field.clean_wl
 """
 import numpy as np
+from numpy.testing import assert_allclose
 
 from jwst.flatfield import flat_field
 
@@ -25,7 +26,7 @@ def test_clean_wl_1():
 
     # 1 means horizontal dispersion
     wl_hc = flat_field.clean_wl(wl_h, 1)
-    assert np.allclose(wl_hc, wl_h_clean, atol=0., rtol=1.e-6)
+    assert_allclose(wl_hc, wl_h_clean, atol=0., rtol=1.e-6)
 
 
 def test_clean_wl_2():
@@ -51,4 +52,4 @@ def test_clean_wl_2():
 
     # 2 means vertical dispersion
     wl_vc = flat_field.clean_wl(wl_v, 2)
-    assert np.allclose(wl_vc, wl_v_clean, atol=0., rtol=1.e-6)
+    assert_allclose(wl_vc, wl_v_clean, atol=0., rtol=1.e-6)
diff --git a/jwst/flatfield/tests/test_combine_fast_slow.py b/jwst/flatfield/tests/test_combine_fast_slow.py
index f23885fa57e..84c007aa8b1 100644
--- a/jwst/flatfield/tests/test_combine_fast_slow.py
+++ b/jwst/flatfield/tests/test_combine_fast_slow.py
@@ -5,6 +5,7 @@
 import pytest
 from scipy.integrate import quad
 from astropy.modeling import polynomial
+from numpy.testing import assert_allclose, assert_array_equal
 from stdatamodels.jwst.datamodels import dqflags
 
 from jwst.flatfield.flat_field import combine_fast_slow
@@ -79,25 +80,25 @@ def test_combine_fast_slow(flat_err_1, flat_err_2):
     assert new_err.shape == wl.shape
 
     # Column 5 is the expected value
-    assert np.allclose(value[:, 5], correct_value, rtol=1.e-8, atol=1.e-8)
-    assert np.all(new_dq[:, 5] == 0)
-    assert np.allclose(new_err[:, 5], err_result, rtol=1.e-8, atol=1.e-8)
+    assert_allclose(value[:, 5], correct_value, rtol=1.e-8, atol=1.e-8)
+    assert_array_equal(new_dq[:, 5], 0)
+    assert_allclose(new_err[:, 5], err_result, rtol=1.e-8, atol=1.e-8)
 
     # Columns 0-2 are bad (negative wavelengths, not marked in DQ)
-    assert np.all(value[:, :3] == 1)
-    assert np.all(new_dq[:, :3] == 0)
+    assert_array_equal(value[:, :3], 1)
+    assert_array_equal(new_dq[:, :3], 0)
     if flat_err is None or np.isnan(flat_err_2):
-        assert np.all(new_err[:, :3] == 0)
+        assert_array_equal(new_err[:, :3], 0)
     else:
-        assert np.allclose(new_err[:, :3], flat_err[:, :3], equal_nan=True)
+        assert_allclose(new_err[:, :3], flat_err[:, :3], equal_nan=True)
 
     # Columns 3, 4, 6-9 are not covered by the tabular data
     # (missing values, marked in DQ)
     bad_value = dqflags.pixel['NO_FLAT_FIELD'] | dqflags.pixel['DO_NOT_USE']
-    assert np.all(value[:, (3, 4, 6, 7, 8, 9)] == 1)
-    assert np.all(new_dq[:, (3, 4, 6, 7, 8, 9)] == bad_value)
+    assert_array_equal(value[:, (3, 4, 6, 7, 8, 9)], 1)
+    assert_array_equal(new_dq[:, (3, 4, 6, 7, 8, 9)], bad_value)
     if flat_err is None or np.isnan(flat_err_2):
-        assert np.all(new_err[:, (3, 4, 6, 7, 8, 9)] == 0)
+        assert_array_equal(new_err[:, (3, 4, 6, 7, 8, 9)], 0)
     else:
-        assert np.allclose(new_err[:, (3, 4, 6, 7, 8, 9)],
-                           flat_err[:, (3, 4, 6, 7, 8, 9)])
+        assert_allclose(new_err[:, (3, 4, 6, 7, 8, 9)],
+                        flat_err[:, (3, 4, 6, 7, 8, 9)])
diff --git a/jwst/flatfield/tests/test_flatfield.py b/jwst/flatfield/tests/test_flatfield.py
index 71a5f8b432d..08c10981435 100644
--- a/jwst/flatfield/tests/test_flatfield.py
+++ b/jwst/flatfield/tests/test_flatfield.py
@@ -2,7 +2,7 @@
 import numpy as np
 from astropy.io import fits
 from astropy.table import Table
-
+from numpy.testing import assert_allclose
 from stdatamodels.jwst import datamodels
 
 from jwst.assign_wcs import AssignWcsStep
@@ -109,12 +109,12 @@ def test_nirspec_flatfield_step_interface(exptype):
     FlatFieldStep.call(data)
 
 
-def create_nirspec_flats(shape, msa=False):
+def create_nirspec_flats(shape, msa=False, flat_data_value=1.0, flat_err_value=0.1):
     flats = []
     for flat_name in ['f', 's', 'd']:
         if flat_name == 'f':
-            f_data = np.full(shape[0], 1.0)
-            f_err = np.full(shape[0], 0.1)
+            f_data = np.full(shape[0], flat_data_value)
+            f_err = np.full(shape[0], flat_err_value)
         else:
             f_data = np.full(shape[0], 1.0)
             f_err = np.full(shape[0], np.nan)
@@ -169,14 +169,13 @@ def create_nirspec_flats(shape, msa=False):
                 hdu.header['EXTVER'] = quadrant + 1
                 fflat_hdul.append(hdu)
 
+            flat = datamodels.NirspecQuadFlatModel(fflat_hdul)
         else:
             fflat_hdul.append(fits.table_to_hdu(flat_table))
             fflat_hdul[-1].name = 'FAST_VARIATION'
 
-        if msa and flat_name == 'f':
-            flat = datamodels.NirspecQuadFlatModel(fflat_hdul)
-        else:
             flat = datamodels.NirspecFlatModel(fflat_hdul)
+
         fflat_hdul.close()
 
         flat.meta.instrument.name = 'NIRSPEC'
@@ -216,7 +215,7 @@ def test_nirspec_bots_flat():
 
     # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.data)
-    assert np.allclose(result.data[nn], data.data[nn])
+    assert_allclose(result.data[nn], data.data[nn])
 
     # check that NaNs match in every extension they should
     for ext in ['data', 'err', 'var_rnoise', 'var_poisson', 'var_flat']:
@@ -227,7 +226,7 @@ def test_nirspec_bots_flat():
     # error is propagated from non-empty fflat error
     # (no other additive contribution, scale from data is 1.0)
     assert result.var_flat.shape == shape
-    assert np.allclose(result.var_flat[nn], 0.1 ** 2)
+    assert_allclose(result.var_flat[nn], 0.1 ** 2)
     assert result.meta.cal_step.flat_field == 'COMPLETE'
 
     result.close()
@@ -252,9 +251,9 @@ def test_nirspec_fs_flat(srctype):
     data.slits.append(datamodels.SlitModel(shape))
     data.slits[0].data = np.full(shape, 1.0)
     data.slits[0].dq = np.full(shape, 0)
-    data.slits[0].err = np.full(shape, 0.0)
-    data.slits[0].var_poisson = np.full(shape, 0.0)
-    data.slits[0].var_rnoise = np.full(shape, 0.0)
+    data.slits[0].var_poisson = np.full(shape, 0.01)
+    data.slits[0].var_rnoise = np.full(shape, 0.01)
+    data.slits[0].err = np.sqrt(data.slits[0].var_poisson + data.slits[0].var_rnoise)
     data.slits[0].data = np.full(shape, 1.0)
     data.slits[0].wavelength = np.ones(shape[-2:])
     data.slits[0].wavelength[:] = np.linspace(1, 5, shape[-1], dtype=float)
@@ -265,13 +264,25 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat[nn], 0)  # orig_input has no var_flat
+    assert_allclose(rt_data.slits[0].err[nn], orig_data.slits[0].err[nn])
 
     # check that NaNs match in every extension they should
     for ext in ['data', 'err', 'var_rnoise', 'var_poisson', 'var_flat']:
@@ -282,7 +293,10 @@ def test_nirspec_fs_flat(srctype):
     # error is propagated from non-empty fflat error
     # (no other additive contribution, scale from data is 1.0)
     assert result.slits[0].var_flat.shape == shape
-    assert np.allclose(result.slits[0].var_flat[nn], 0.1 ** 2)
+    assert_allclose(
+        result.slits[0].var_flat[nn],
+        ((1 / flat_val) / flat_val * flat_err) ** 2,
+        rtol=1e-6)
     assert result.meta.cal_step.flat_field == 'COMPLETE'
 
     result.close()
@@ -328,7 +342,7 @@ def test_nirspec_msa_flat():
 
     # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], data.slits[0].data[nn])
 
     # check that NaNs match in every extension they should
     for ext in ['data', 'err', 'var_rnoise', 'var_poisson', 'var_flat']:
@@ -339,7 +353,7 @@ def test_nirspec_msa_flat():
     # error is propagated from non-empty fflat error
     # (no other additive contribution, scale from data is 1.0)
     assert result.slits[0].var_flat.shape == shape
-    assert np.allclose(result.slits[0].var_flat[nn], 0.1 ** 2)
+    assert_allclose(result.slits[0].var_flat[nn], 0.1 ** 2)
     assert result.meta.cal_step.flat_field == 'COMPLETE'
 
     result.close()
@@ -372,7 +386,7 @@ def test_nirspec_ifu_flat():
 
     # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.data)
-    assert np.allclose(result.data[nn], data.data[nn])
+    assert_allclose(result.data[nn], data.data[nn])
 
     # check that NaNs match in every extension they should
     for ext in ['data', 'err', 'var_rnoise', 'var_poisson', 'var_flat']:
@@ -383,7 +397,7 @@ def test_nirspec_ifu_flat():
     # error is propagated from non-empty fflat error
     # (no other additive contribution, scale from data is 1.0)
     assert result.var_flat.shape == shape
-    assert np.allclose(result.var_flat[nn], 0.1 ** 2)
+    assert_allclose(result.var_flat[nn], 0.1 ** 2)
     assert result.meta.cal_step.flat_field == 'COMPLETE'
 
     result.close()
diff --git a/jwst/flatfield/tests/test_interpolate_flat.py b/jwst/flatfield/tests/test_interpolate_flat.py
index a49de6aa092..266c69c8473 100644
--- a/jwst/flatfield/tests/test_interpolate_flat.py
+++ b/jwst/flatfield/tests/test_interpolate_flat.py
@@ -1,8 +1,8 @@
 """
 Test for flat_field.interpolate_flat
 """
-
 import numpy as np
+from numpy.testing import assert_allclose
 
 from jwst.flatfield.flat_field import interpolate_flat
 
@@ -22,9 +22,9 @@ def test_interpolate_flat_1():
 
     # Since image_flat is 2-D, the inputs will be returned unchanged.
     output = interpolate_flat(image_flat, image_dq, image_err, image_wl, wl)
-    assert np.allclose(output[0], image_flat, atol=1.0e-6)
-    assert np.allclose(output[1], image_dq, atol=0)
-    assert np.allclose(output[2], image_err, atol=1.0e-6)
+    assert_allclose(output[0], image_flat, atol=1e-6)
+    assert_allclose(output[1], image_dq, atol=0)
+    assert_allclose(output[2], image_err, atol=1e-6)
 
 
 def test_interpolate_flat_2():
@@ -35,9 +35,9 @@ def test_interpolate_flat_2():
     # 2-D
     image_dq = np.arange(nx * ny, dtype=np.int32).reshape(ny, nx)
     output = interpolate_flat(image_flat, image_dq, image_err, image_wl, wl)
-    assert np.allclose(output[0], image_flat[0, :, :], atol=1.0e-6)
-    assert np.allclose(output[1], image_dq, atol=0)
-    assert np.allclose(output[2], image_err[0, :, :], atol=1.0e-6)
+    assert_allclose(output[0], image_flat[0, :, :], atol=1e-6)
+    assert_allclose(output[1], image_dq, atol=0)
+    assert_allclose(output[2][0, :, :], image_err[0, :, :], atol=1e-6)
 
 
 def test_interpolate_flat_3():
@@ -48,9 +48,9 @@ def test_interpolate_flat_3():
     # 2-D, but reshaped to 3-D
     image_dq = np.arange(nx * ny, dtype=np.int32).reshape(1, ny, nx)
     output = interpolate_flat(image_flat, image_dq, image_err, image_wl, wl)
-    assert np.allclose(output[0], image_flat[0, :, :], atol=1.0e-6)
-    assert np.allclose(output[1], image_dq[0, :, :], atol=0)
-    assert np.allclose(output[2], image_err[0, :, :], atol=1.0e-6)
+    assert_allclose(output[0], image_flat[0, :, :], atol=1e-6)
+    assert_allclose(output[1], image_dq[0, :, :], atol=0)
+    assert_allclose(output[2], image_err[0, :, :], atol=1e-6)
 
 
 def test_interpolate_flat_4():
@@ -95,6 +95,6 @@ def test_interpolate_flat_4():
         dtype=np.float32,
     )
 
-    assert np.allclose(output[0], expected_value_0, atol=1.0e-6)
-    assert np.allclose(output[1], expected_value_1, atol=0)
-    assert np.allclose(output[2], expected_value_2, atol=1.0e-6)
+    assert_allclose(output[0], expected_value_0, atol=1e-6)
+    assert_allclose(output[1], expected_value_1, atol=0)
+    assert_allclose(output[2], expected_value_2, atol=1e-6)
diff --git a/jwst/photom/photom.py b/jwst/photom/photom.py
index 63e368bef09..48787b3b15a 100644
--- a/jwst/photom/photom.py
+++ b/jwst/photom/photom.py
@@ -80,40 +80,33 @@ def _normalize_strings(field):
     return row[0]
 
 
-class DataSet():
+class DataSet:
     """
-    Input dataset to which the photom information will be applied
+    Input dataset to which the photom information will be applied.
+
+    Store vital params, such as
+    instrument, detector, filter, pupil, and exposure type.
 
     Parameters
     ----------
+    model : `~jwst.datamodels.JwstDataModel`
+        Input Data Model object.
 
-    """
+    inverse : bool
+        Invert the math operations used to apply the corrections.
 
-    def __init__(self, model, inverse=False, source_type=None, mrs_time_correction=False,
-                 correction_pars=None):
-        """
-        Short Summary
-        -------------
-        Store vital params in DataSet object, such as
-        instrument, detector, filter, pupil, and exposure type.
-
-        Parameters
-        ----------
-        model : `~jwst.datamodels.JwstDataModel`
-            input Data Model object
+    source_type : str or None
+        Force processing using the specified source type.
 
-        inverse : boolean
-            Invert the math operations used to apply the corrections.
+    mrs_time_correction : bool
+        Switch to apply/not apply the MRS time correction.
 
-        source_type : str or None
-            Force processing using the specified source type.
-
-        mrs_time_correction: bool
-            Switch to apply/not apply the mrs time correction
+    correction_pars : dict
+        Correction meta-data from a previous run.
+    """
 
-        correction_pars : dict
-            Correction meta-data from a previous run.
-        """
+    def __init__(self, model, inverse=False, source_type=None, mrs_time_correction=False,
+                 correction_pars=None):
         # Setup attributes necessary for calculation.
         if correction_pars:
             self.update(correction_pars['dataset'])
@@ -189,7 +182,8 @@ def __init__(self, model, inverse=False, source_type=None, mrs_time_correction=F
 
     @property
     def attributes(self):
-        """Retrieve DataSet attributes
+        """
+        Retrieve DataSet attributes.
 
         Returns
         -------
@@ -206,20 +200,19 @@ def attributes(self):
         return attributes
 
     def update(self, attributes):
-        """Set DataSet attributes
+        """
+        Set DataSet attributes.
 
         Parameters
         ----------
         attributes : dict
-            The attributes to be set on DataSet
+            The attributes to be set on DataSet.
         """
         for key, value in attributes.items():
             setattr(self, key, value)
 
     def calc_nirspec(self, ftab, area_fname):
         """
-        Extended Summary
-        -------------
         For the NIRSPEC instrument, reference file matching is based on
         FILTER and GRATING, as well as SLIT name for the fixed-slits mode.
         The routine will find the corresponding information in the reference
@@ -230,14 +223,10 @@ def calc_nirspec(self, ftab, area_fname):
         Parameters
         ----------
         ftab : `~jwst.datamodels.NrsFsPhotomModel` or `~jwst.datamodels.NrsMosPhotomModel`
-            NIRSpec photom reference file data model
+            NIRSpec photom reference file data model.
 
         area_fname : str
-            Pixel area map reference file name
-
-        Returns
-        -------
-
+            Pixel area map reference file name.
         """
 
         # Normal fixed-slit exposures get handled as a MultiSlitModel
@@ -351,15 +340,23 @@ def calc_nirspec(self, ftab, area_fname):
                 self.input.dq = np.bitwise_or(self.input.dq, dqmap)
 
                 # Multiply the science data and uncertainty arrays by the conversion factors
+                sens2d_squared = sens2d * sens2d
                 if not self.inverse:
                     self.input.data *= sens2d
+                    self.input.err *= sens2d
+                    self.input.var_poisson *= sens2d_squared
+                    self.input.var_rnoise *= sens2d_squared
                 else:
                     self.input.data /= sens2d
-                self.input.err *= sens2d
-                self.input.var_poisson *= sens2d**2
-                self.input.var_rnoise *= sens2d**2
+                    self.input.err /= sens2d
+                    self.input.var_poisson /= sens2d_squared
+                    self.input.var_rnoise /= sens2d_squared
+
                 if self.input.var_flat is not None and np.size(self.input.var_flat) > 0:
-                    self.input.var_flat *= sens2d**2
+                    if not self.inverse:
+                        self.input.var_flat *= sens2d_squared
+                    else:
+                        self.input.var_flat /= sens2d_squared
 
                 # Update BUNIT values for the science data and err
                 if not self.inverse:
@@ -377,8 +374,6 @@ def calc_nirspec(self, ftab, area_fname):
 
     def calc_niriss(self, ftab):
         """
-        Extended Summary
-        -------------
         For NIRISS matching is based on FILTER and PUPIL, as well as ORDER
         for spectroscopic modes.
         There may be multiple entries for a given FILTER+PUPIL combination,
@@ -393,13 +388,8 @@ def calc_niriss(self, ftab):
 
         Parameters
         ----------
-        ftab : `~jwst.datamodels.NisSossPhotomModel` or
-               `~jwst.datamodels.NisWfssPhotomModel` or
-               `~jwst.datamodels.NisImgPhotomModel`
-            NIRISS photom reference file data model
-
-        Returns
-        -------
+        ftab : `~jwst.datamodels.NisSossPhotomModel` or `~jwst.datamodels.NisWfssPhotomModel` or `~jwst.datamodels.NisImgPhotomModel`
+            NIRISS photom reference file data model.
         """
 
         # Handle MultiSlit models separately, which are used for NIRISS WFSS
@@ -457,9 +447,7 @@ def calc_miri(self, ftab):
 
         Parameters
         ----------
-        ftab : `~jwst.datamodels.MirImgPhotomModel` or
-               `~jwst.datamodels.MirMrsPhotomModel` or
-               `~jwst.datamodels.MirLrsPhotomModel`
+        ftab : `~jwst.datamodels.MirImgPhotomModel` or `~jwst.datamodels.MirMrsPhotomModel` or `~jwst.datamodels.MirLrsPhotomModel`
             MIRI photom reference file data model.
 
         """
@@ -521,15 +509,26 @@ def calc_miri(self, ftab):
 
             # Multiply the science data and uncertainty arrays by the 2D
             # sensitivity factors
+            sens2d_squared = sens2d * sens2d
             if not self.inverse:
                 self.input.data *= sens2d
+                self.input.err *= sens2d
+                self.input.var_poisson *= sens2d_squared
+                self.input.var_rnoise *= sens2d_squared
             else:
                 self.input.data /= sens2d
-            self.input.err *= sens2d
-            self.input.var_poisson *= sens2d**2
-            self.input.var_rnoise *= sens2d**2
+                self.input.err /= sens2d
+                self.input.var_poisson /= sens2d_squared
+                self.input.var_rnoise /= sens2d_squared
+
             if self.input.var_flat is not None and np.size(self.input.var_flat) > 0:
-                self.input.var_flat *= sens2d**2
+                process_var_flat = True
+                if not self.inverse:
+                    self.input.var_flat *= sens2d_squared
+                else:
+                    self.input.var_flat /= sens2d_squared
+            else:
+                process_var_flat = False
 
             # Update the science dq
             self.input.dq = np.bitwise_or(self.input.dq, ftab.dq)
@@ -554,7 +553,7 @@ def calc_miri(self, ftab):
                 self.input.err /= correction
                 self.input.var_poisson *= inv_correction_sq
                 self.input.var_rnoise *= inv_correction_sq
-                if self.input.var_flat is not None and np.size(self.input.var_flat) > 0:
+                if process_var_flat:
                     self.input.var_flat *= inv_correction_sq
             else:
                 log.info("Not applying MRS IFU time dependent correction.")
@@ -578,8 +577,6 @@ def calc_miri(self, ftab):
 
     def calc_nircam(self, ftab):
         """
-        Extended Summary
-        -------------
         For NIRCAM, matching is based on FILTER and PUPIL.
         The routine will find the corresponding information in the reference
         file, apply the conversion factors, and store the scalar conversion
@@ -590,12 +587,8 @@ def calc_nircam(self, ftab):
 
         Parameters
         ----------
-        ftab : `~jwst.datamodels.NrcImgPhotomModel` or
-               `~jwst.datamodels.NrcWfssPhotomModel`
-            NIRCam photom reference file data model
-
-        Returns
-        -------
+        ftab : `~jwst.datamodels.NrcImgPhotomModel` or `~jwst.datamodels.NrcWfssPhotomModel`
+            NIRCam photom reference file data model.
         """
         # Handle WFSS data separately from regular imaging
         if isinstance(self.input, datamodels.MultiSlitModel) and self.exptype == 'NRC_WFSS':
@@ -626,8 +619,6 @@ def calc_nircam(self, ftab):
 
     def calc_fgs(self, ftab):
         """
-        Extended Summary
-        -------------
         For FGS, there is no matching required, because the instrument does
         not contain any filters or pupil wheel elements. The only mode is CLEAR.
 
@@ -638,37 +629,34 @@ def calc_fgs(self, ftab):
         Parameters
         ----------
         ftab : `~jwst.datamodels.FgsImgPhotomModel`
-            FGS photom reference file data model
-
-        Returns
-        -------
+            FGS photom reference file data model.
         """
 
         # Read the first (and only) row in the reference file
         self.photom_io(ftab.phot_table[0])
 
     def calc_nrs_ifu_sens2d(self, area_data):
-        """Create the 2-D wavelength and pixel area arrays needed for
+        """
+        Create the 2-D wavelength and pixel area arrays needed for
         constructing a NIRSpec IFU sensitivity map.
 
         Parameters
         ----------
         area_data : 1-D ndarray
-            Array of pixel area values for the IFU slices
+            Array of pixel area values for the IFU slices.
 
         Returns
         -------
         wave2d : 2-D ndarray
-            Array of wavelengths per pixel
+            Array of wavelengths per pixel.
 
         area2d : 2-D ndarray
-            Array of pixel area values
+            Array of pixel area values.
 
         dqmap : 2-D ndarray
-            Array of DQ flags per pixel
+            Array of DQ flags per pixel.
         """
 
-        import numpy as np
         from .. assign_wcs import nirspec       # for NIRSpec IFU data
         import gwcs
 
@@ -732,22 +720,16 @@ def calc_nrs_ifu_sens2d(self, area_data):
 
     def photom_io(self, tabdata, order=None):
         """
-        Short Summary
-        -------------
         Combine photometric scalar and wavelength-dependent conversion factors
         and apply to the science dataset.
 
         Parameters
         ----------
         tabdata : FITS record
-            Single row of data from reference table
+            Single row of data from reference table.
 
         order : int
-            Spectral order number
-
-        Returns
-        -------
-
+            Spectral order number.
         """
         # First get the scalar conversion factor.
         # For most modes, the scalar conversion factor in the photom reference
@@ -915,17 +897,28 @@ def photom_io(self, tabdata, order=None):
         # Apply the conversion to the data and all uncertainty arrays
         if isinstance(self.input, datamodels.MultiSlitModel):
             slit = self.input.slits[self.slitnum]
+            conversion_squared = conversion * conversion
             if not self.inverse:
                 slit.data *= conversion
+                slit.err *= conversion
             else:
                 slit.data /= conversion
-            slit.err *= conversion
+                slit.err /= conversion
             if slit.var_poisson is not None and np.size(slit.var_poisson) > 0:
-                slit.var_poisson *= conversion**2
+                if not self.inverse:
+                    slit.var_poisson *= conversion_squared
+                else:
+                    slit.var_poisson /= conversion_squared
             if slit.var_rnoise is not None and np.size(slit.var_rnoise) > 0:
-                slit.var_rnoise *= conversion**2
+                if not self.inverse:
+                    slit.var_rnoise *= conversion_squared
+                else:
+                    slit.var_rnoise /= conversion_squared
             if slit.var_flat is not None and np.size(slit.var_flat) > 0:
-                slit.var_flat *= conversion**2
+                if not self.inverse:
+                    slit.var_flat *= conversion_squared
+                else:
+                    slit.var_flat /= conversion_squared
             if no_cal is not None:
                 slit.dq[..., no_cal] = np.bitwise_or(slit.dq[..., no_cal],
                                                      dqflags.pixel['DO_NOT_USE'])
@@ -933,17 +926,13 @@ def photom_io(self, tabdata, order=None):
                 if unit_is_surface_brightness:
                     slit.meta.bunit_data = 'MJy/sr'
                     slit.meta.bunit_err = 'MJy/sr'
-                    # Setting top model to None so they will not be written to FITs File.
-                    # Information on the units should only come from the individual slits.
-                    self.input.meta.bunit_data = None
-                    self.input.meta.bunit_err = None
                 else:
                     slit.meta.bunit_data = 'MJy'
                     slit.meta.bunit_err = 'MJy'
-                    # Setting top model to None so they will not be written to FITs File.
-                    # Information on the units should only come from the individual slits.
-                    self.input.meta.bunit_data = None
-                    self.input.meta.bunit_err = None
+                # Setting top model to None so they will not be written to FITs File.
+                # Information on the units should only come from the individual slits.
+                self.input.meta.bunit_data = None
+                self.input.meta.bunit_err = None
             else:
                 self.input.meta.bunit_data = 'DN/s'
                 self.input.meta.bunit_err = 'DN/s'
@@ -978,17 +967,29 @@ def photom_io(self, tabdata, order=None):
             spec.spec_table.columns['BKGD_VAR_FLAT'].unit = 'MJy^2'
 
         else:
+            conversion_squared = conversion * conversion
             if not self.inverse:
                 self.input.data *= conversion
+                self.input.err *= conversion
+
             else:
                 self.input.data /= conversion
-            self.input.err *= conversion
+                self.input.err /= conversion
             if self.input.var_poisson is not None and np.size(self.input.var_poisson) > 0:
-                self.input.var_poisson *= conversion**2
+                if not self.inverse:
+                    self.input.var_poisson *= conversion_squared
+                else:
+                    self.input.var_poisson /= conversion_squared
             if self.input.var_rnoise is not None and np.size(self.input.var_rnoise) > 0:
-                self.input.var_rnoise *= conversion**2
+                if not self.inverse:
+                    self.input.var_rnoise *= conversion_squared
+                else:
+                    self.input.var_rnoise /= conversion_squared
             if self.input.var_flat is not None and np.size(self.input.var_flat) > 0:
-                self.input.var_flat *= conversion**2
+                if not self.inverse:
+                    self.input.var_flat *= conversion_squared
+                else:
+                    self.input.var_flat /= conversion_squared
             if no_cal is not None:
                 self.input.dq[..., no_cal] = np.bitwise_or(self.input.dq[..., no_cal],
                                                            dqflags.pixel['DO_NOT_USE'])
@@ -1007,8 +1008,6 @@ def photom_io(self, tabdata, order=None):
             # Make sure output model has consistent NaN and DO_NOT_USE values
             match_nans_and_flags(self.input)
 
-        return
-
     def create_2d_conversion(self, model, exptype, conversion, waves, relresps,
                              order, use_wavecorr=None, include_dispersion=False):
         """
@@ -1018,18 +1017,18 @@ def create_2d_conversion(self, model, exptype, conversion, waves, relresps,
         Parameters
         ----------
         model : `~jwst.datamodels.JwstDataModel`
-            Input data model containing the necessary wavelength information
+            Input data model containing the necessary wavelength information.
         exptype : str
-            Exposure type of the input
+            Exposure type of the input.
         conversion : float
-            Initial scalar photometric conversion value
+            Initial scalar photometric conversion value.
         waves : float
             1D wavelength vector on which relative response values are
-            sampled
+            sampled.
         relresps : float
-            1D photometric response values, as a function of waves
+            1D photometric response values, as a function of waves.
         order : int
-            Spectral order number
+            Spectral order number.
         use_wavecorr : bool or None
             Flag indicating whether or not to use corrected wavelengths.
             Typically only used for NIRSpec fixed-slit data.
@@ -1040,9 +1039,9 @@ def create_2d_conversion(self, model, exptype, conversion, waves, relresps,
         Returns
         -------
         conversion : float
-            2D array of computed photometric conversion values
+            2D array of computed photometric conversion values.
         no_cal : int
-            2D mask indicating where no conversion is available
+            2D mask indicating where no conversion is available.
         """
 
         # Get the 2D wavelength array corresponding to the input
@@ -1072,20 +1071,20 @@ def create_2d_conversion(self, model, exptype, conversion, waves, relresps,
         return conversion, no_cal
 
     def get_dispersion_array(self, wavelength_array, dispaxis):
-        """Create an array of dispersion values from the wavelength array
+        """
+        Create an array of dispersion values from the wavelength array.
 
         Parameters
         ----------
         wavelength_array : float
-            2-d array of wavelength values, assumed to be in microns
-        dispaxis : integer
-            Direction along which light is dispersed: 1 = along rows, 2 = along columns
+            2-d array of wavelength values, assumed to be in microns.
+        dispaxis : int
+            Direction along which light is dispersed: 1 = along rows, 2 = along columns.
 
         Returns
         -------
         dispersion_array : float
-            2-d array of dispersion values, in microns/pixel
-
+            2-d array of dispersion values, in microns/pixel.
         """
         nrows, ncols = wavelength_array.shape
         dispersion_array = np.zeros(wavelength_array.shape)
@@ -1100,27 +1099,28 @@ def get_dispersion_array(self, wavelength_array, dispaxis):
         return dispersion_array
 
     def create_1d_conversion(self, model, conversion, waves, relresps):
-        """Create a 1D array of photometric conversion values based on
+        """
+        Create a 1D array of photometric conversion values based on
         wavelength array of input spectrum and response as a function of wavelength.
 
         Parameters
         ----------
         model : `~jwst.datamodels.JwstDataModel`
-            Input data model containing the necessary wavelength information
+            Input data model containing the necessary wavelength information.
         conversion : float
-            Initial scalar photometric conversion value
+            Initial scalar photometric conversion value.
         waves : float
             1D wavelength vector on which relative response values are
-            sampled
+            sampled.
         relresps : float
-            1D photometric response values, as a function of waves
+            1D photometric response values, as a function of waves.
 
         Returns
         -------
         conversion : float
-            1D array of computed photometric conversion values
+            1D array of computed photometric conversion values.
         no_cal : int
-            1D mask indicating where no conversion is available
+            1D mask indicating where no conversion is available.
         """
 
         # Get the 2D wavelength array corresponding to the input
@@ -1158,15 +1158,13 @@ def create_1d_conversion(self, model, conversion, waves, relresps):
 
     def pixarea_from_ftab(self, ftab):
         """
-        Short Summary
-        -------------
         Read the pixel area values in the PIXAR_A2 and PIXAR_SR keys from the
         primary header of the photom reference file.
 
         Parameters
         ----------
         ftab : `~jwst.datamodels.JwstDataModel`
-            A photom reference file data model
+            A photom reference file data model.
         """
         area_ster, area_a2 = None, None
         area_ster = ftab.meta.photometry.pixelarea_steradians
@@ -1182,8 +1180,6 @@ def pixarea_from_ftab(self, ftab):
 
     def save_area_info(self, ftab, area_fname):
         """
-        Short Summary
-        -------------
         Read the pixel area values in the PIXAR_A2 and PIXAR_SR keys from the
         primary header of the pixel area reference file or (for NIRSpec data)
         from the PIXAREA column in the selected row of the AREA table in the
@@ -1196,10 +1192,10 @@ def save_area_info(self, ftab, area_fname):
         Parameters
         ----------
         ftab : `~jwst.datamodels.JwstDataModel`
-            A photom reference file data model
+            A photom reference file data model.
 
         area_fname : str
-            Pixel area reference file name
+            Pixel area reference file name.
         """
 
         use_pixarea_rfile =  False
@@ -1270,8 +1266,6 @@ def save_area_info(self, ftab, area_fname):
 
     def save_area_nirspec(self, pix_area):
         """
-        Short Summary
-        -------------
         Read the pixel area value from the PIXAREA column in the selected row
         of the AREA table in the area reference file.
         Use that information to populate the pixel area keywords in the output
@@ -1280,7 +1274,7 @@ def save_area_nirspec(self, pix_area):
         Parameters
         ----------
         pix_area : `~jwst.datamodels.JwstDataModel`
-            Pixel area reference file data model
+            Pixel area reference file data model.
         """
 
         exp_type = self.exptype
@@ -1361,8 +1355,6 @@ def save_area_nirspec(self, pix_area):
 
     def apply_photom(self, photom_fname, area_fname):
         """
-        Short Summary
-        -------------
         Open the reference file, retrieve the conversion factors from the reference
         file that are appropriate to the instrument mode. This can consist of both
         a scalar factor and an array of wavelength-dependent factors. The combination
@@ -1374,46 +1366,42 @@ def apply_photom(self, photom_fname, area_fname):
         Parameters
         ----------
         photom_fname : str
-            photom reference file name
+            photom reference file name.
 
-        area_fname: str
-            pixel area map reference file name
+        area_fname : str
+            Pixel area map reference file name.
 
         Returns
         -------
         output_model : ~jwst.datamodels.JwstDataModel
-            output data model with the flux calibrations applied
-
+            Output data model with the flux calibrations applied.
         """
 
-        ftab = datamodels.open(photom_fname)
-
-        # Load the pixel area reference file, if it exists, and attach the
-        # reference data to the science model
-        # SOSS data are in a MultiSpecModel, which will not allow for
-        # saving the area info.
-        if self.exptype != 'NIS_SOSS':
-            self.save_area_info(ftab, area_fname)
+        with datamodels.open(photom_fname) as ftab:
 
-        if self.instrument == 'NIRISS':
-            self.calc_niriss(ftab)
+            # Load the pixel area reference file, if it exists, and attach the
+            # reference data to the science model
+            # SOSS data are in a MultiSpecModel, which will not allow for
+            # saving the area info.
+            if self.exptype != 'NIS_SOSS':
+                self.save_area_info(ftab, area_fname)
 
-        elif self.instrument == 'NIRSPEC':
-            self.calc_nirspec(ftab, area_fname)
+            if self.instrument == 'NIRISS':
+                self.calc_niriss(ftab)
 
-        elif self.instrument == 'NIRCAM':
-            self.calc_nircam(ftab)
+            elif self.instrument == 'NIRSPEC':
+                self.calc_nirspec(ftab, area_fname)
 
-        elif self.instrument == 'MIRI':
-            self.calc_miri(ftab)
+            elif self.instrument == 'NIRCAM':
+                self.calc_nircam(ftab)
 
-        elif self.instrument == 'FGS':
-            self.calc_fgs(ftab)
+            elif self.instrument == 'MIRI':
+                self.calc_miri(ftab)
 
-        else:
-            raise RuntimeError('Instrument {} is not recognized'
-                               .format(self.instrument))
+            elif self.instrument == 'FGS':
+                self.calc_fgs(ftab)
 
-        ftab.close()  # Close the photom reference table
+            else:
+                raise RuntimeError(f'Instrument {self.instrument} is not recognized')
 
         return self.input
diff --git a/jwst/photom/tests/test_photom.py b/jwst/photom/tests/test_photom.py
index b2da6d8069a..3c210a597e5 100644
--- a/jwst/photom/tests/test_photom.py
+++ b/jwst/photom/tests/test_photom.py
@@ -2,9 +2,8 @@
 
 import pytest
 import numpy as np
-
 from astropy import units as u
-
+from numpy.testing import assert_allclose
 from stdatamodels.jwst import datamodels
 from stdatamodels.jwst.datamodels import SpecModel, MultiSpecModel
 
@@ -1315,7 +1314,7 @@ def test_niriss_soss():
     compare = photmj * rel_resp
     # Compare the values at the center pixel.
     ratio = output[test_ind] / input[test_ind]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_niriss_image():
@@ -1339,7 +1338,7 @@ def test_niriss_image():
     compare = photmjsr
     # Compare the values at the center pixel.
     ratio = output[iy, ix] / input[iy, ix]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_expected_failure_niriss_cubemodel():
@@ -1436,7 +1435,7 @@ def test_miri_lrs():
     compare = photmjsr * rel_resp
     # Compare the values at the center pixel.
     ratio = output[iy, ix] / input[iy, ix]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_miri_image():
@@ -1464,7 +1463,7 @@ def test_miri_image():
     compare = photmjsr + amplitude * np.exp(-(60000 - t0) / tau)  # Added for new PHOTOM step
     # Compare the values at the center pixel.
     ratio = output[iy, ix] / input[iy, ix]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_nircam_image():
@@ -1488,7 +1487,7 @@ def test_nircam_image():
     compare = photmjsr
     # Compare the values at the center pixel.
     ratio = output[iy, ix] / input[iy, ix]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_nircam_spec():
@@ -1529,7 +1528,7 @@ def test_nircam_spec():
         compare = photmjsr * rel_resp / disp
         # Compare the values at the center pixel.
         ratio = output[iy, ix] / input[iy, ix]
-        assert np.allclose(ratio, compare, rtol=1.e-7)
+        assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_fgs():
@@ -1554,7 +1553,7 @@ def test_fgs():
     compare = photmjsr
     # Compare the values at the center pixel.
     ratio = output[iy, ix] / input[iy, ix]
-    assert np.allclose(ratio, compare, rtol=1.e-7)
+    assert_allclose(ratio, compare, rtol=1.e-7)
 
 
 def test_apply_photom_1():
@@ -1583,16 +1582,25 @@ def test_apply_photom_1():
     # (and other photom models) can take either an open model or the name of
     # a file as input.
     output_model = ds.apply_photom(ftab, area_ref)
-    assert (math.isclose(output_model.meta.photometry.pixelarea_steradians,
-                         area_ster, rel_tol=1.e-7))
-    assert (math.isclose(output_model.meta.photometry.pixelarea_arcsecsq,
-                         area_a2, rel_tol=1.e-7))
+    assert_allclose(output_model.meta.photometry.pixelarea_steradians,
+                    area_ster, rtol=1e-7)
+    assert_allclose(output_model.meta.photometry.pixelarea_arcsecsq,
+                    area_a2, rtol=1e-7)
 
     # check the x,y size of area array in output is the same size as the sci data
     sh_area = output_model.area.shape
     assert shape[-1] == sh_area[-1]
     assert shape[-2] == sh_area[-2]
 
+    # Recover original input before photom step.
+    ds2 = photom.DataSet(output_model, inverse=True)
+    rt_model = ds2.apply_photom(ftab, area_ref)
+    assert_allclose(rt_model.data, input_model.data)
+    assert_allclose(rt_model.err, input_model.err)
+    assert_allclose(rt_model.var_poisson, input_model.var_poisson)
+    assert_allclose(rt_model.var_rnoise, input_model.var_rnoise)
+    assert_allclose(rt_model.var_flat, input_model.var_flat)
+
 
 @pytest.mark.parametrize('srctype', ['POINT', 'EXTENDED'])
 def test_apply_photom_2(srctype):
@@ -1653,6 +1661,16 @@ def test_apply_photom_2(srctype):
     wavelength = ftab.phot_table['wavelength'][rownum][0:nelem]
     relresponse = ftab.phot_table['relresponse'][rownum][0:nelem]
 
+    # Recover original input before photom step.
+    ds2 = photom.DataSet(output_model, inverse=True)
+    rt_model = ds2.apply_photom(ftab, area_ref)
+    for (k, slit) in enumerate(save_input.slits):
+        assert_allclose(rt_model.slits[k].data, slit.data, rtol=1e-4)
+        assert_allclose(rt_model.slits[k].err, slit.err)
+        assert_allclose(rt_model.slits[k].var_poisson, slit.var_poisson)
+        assert_allclose(rt_model.slits[k].var_rnoise, slit.var_rnoise)
+        assert_allclose(rt_model.slits[k].var_flat, slit.var_flat)
+
     result = []
     for (k, slit) in enumerate(save_input.slits):
         input = slit.data                       # this is from save_input


Reviews:
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T00:55:39Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T00:58:45Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T02:16:26Z
user: login: drlaw1558
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T12:22:33Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T14:29:35Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T14:34:18Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T14:35:14Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T17:20:30Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T17:56:49Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T18:05:30Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T18:16:12Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T19:27:45Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T19:28:18Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T19:29:23Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T21:54:36Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T14:00:35Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T14:04:27Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T15:03:23Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T15:06:35Z
user: login: melanieclarke
body: I still don't see why we would be seeing differences in the SOSS products - maybe it's a precision thing with some of the pre-computed squares?  Can you try to isolate which change that might be coming from?  I can look into expected values for the masterbg product.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T18:21:31Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T20:37:05Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T12:43:47Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:06:01Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:11:05Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:13:29Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:17:48Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:32:38Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T19:10:13Z
user: login: melanieclarke
body: Okay, I looked at the masterbg2d output and I think it looks right.  The S/N with the updated error is comparable to what it should be, looking at the extracted spectra it's derived from.   The error and variance arrays also no longer show the influence of the var_flat array, and err is the sum in quadrature of the read noise and poisson variance. The old version had much higher error and variance values, making the S/N way off for the master bg product.

One thing: setting the var_flat to None makes it now write the extension out as a zero-size array, which behaves badly with some FITS readers.  I'm sorry to make you change this again, but I think it would be clearer/easier on users to write out zeros for the var_flat values instead of an empty array.  This will need one more change to the unit test as well, because if the var_flat extension matches the data size, it will update NaN values to match.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T20:59:08Z
user: login: pllim
body: This is just so I can commit all the suggestions at once and keep Melanie as co-author via the GH UI.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T21:06:28Z
user: login: melanieclarke
body: I think this is all ready to go now.  Thanks for working it through with me!  I will resolve the JP ticket and okify the masterbg2d diff.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-16T14:15:01Z

Review comments:
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data ** 2
     if not inverse:
         science.data /= flat_data
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
path: jwst/flatfield/flat_field.py
user: login: pllim
body: I am trying to grok the math. In the original code, the math was

```python
science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
```

that also happens after

```python
science.data /= flat_data
```

Why divide by `flat_data_squared` again in `var_flat` calculation when `science.data` itself was already divided by `flat_data` in-place?
created_at: 2025-04-09T00:55:39Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data ** 2
     if not inverse:
         science.data /= flat_data
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
     else:
         science.data *= flat_data
+        science.var_flat = science.data ** 2 * flat_data_squared * flat_err ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
path: jwst/flatfield/flat_field.py
user: login: pllim
body: While I understand that Poisson noise follows data value, I also don't understand why `flat_data_squared` should be applied to read noise. Isn't read noise hardware related and is not affected by flatfield?
created_at: 2025-04-09T00:58:45Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: pllim
body: I think this `slit.var_flat` calculation caused INF in some of the regtest. I can turn it into NaN like this but I am not sure if it is correct way to handle. As Melanie pointed out, the NaN propagates and also then affects subsequent processing.
created_at: 2025-04-09T02:16:26Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data ** 2
     if not inverse:
         science.data /= flat_data
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
     else:
         science.data *= flat_data
+        science.var_flat = science.data ** 2 * flat_data_squared * flat_err ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
path: jwst/flatfield/flat_field.py
user: login: drlaw1558
body: The read noise itself is hardware related and doesn't 'see' the flatfield, but you can think of it as a unit conversion.  If the SNR of a pixel is 10 prior to flatfielding, you want to ensure that it's still 10 after application of the flatfield too, which requires applying the conversion factor to both numerator and denominator.  (Ignoring here the noise introduced by the flatfield step itself, which is a separate term).
created_at: 2025-04-09T12:22:33Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data ** 2
     if not inverse:
         science.data /= flat_data
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: This is an error propagation thing, for the uncertainty on a quantity A/B when both A and B have associated errors. See for example: https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae
created_at: 2025-04-09T14:29:34Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: Shouldn't that last `*` be a `/`?
created_at: 2025-04-09T14:34:18Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: i.e.: `slit.var_flat = (slit.data * slit_flat.data / slit_flat.err) ** 2`
created_at: 2025-04-09T14:35:13Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: pllim
body: The original math in the code (presumable for `inverse=False`) is:

```python
slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
```

I am not sure what inverse actually mean in terms of pipeline here. If I remember error propagation correctly, regardless of whether it is `x * y` or `x / y`, `x_err` and `y_err` are compounded the same way, right? So I don't understand why we have to divide by `.err` on inverse here. I thought we are only inversing the `.data` stuff. 🤷‍♀️ 
created_at: 2025-04-09T17:20:30Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: On inverse, we're trying to recover the original data and error values, as if the flat processing never happened.  For the error and variance, it's not a propagation process, it's just an undo.

There will be some pixels that are not invertible since the flat process introduces NaNs, and that's okay - we should just return NaNs there.  But for any values with sensible numbers we should aim for data == flat.inverse(flat(data)), error == flat.inverse(flat(error)), variance == flat.inverse(flat(variance)).  If that makes sense?
created_at: 2025-04-09T17:56:49Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: pllim
body: > data == flat.inverse(flat(data))

But. Why. 😅 

Okay... I will fix the math. Thanks!
created_at: 2025-04-09T18:05:30Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data * slit_flat.err) ** 2
+            np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: I know that was rhetorical, but if you want to know the why... 

See the master_background_mos processing, here:
https://github.com/spacetelescope/jwst/blob/929b3f6e83db1f2e75c9d529e496df61cc434bb8/jwst/master_background/master_background_mos_step.py#L253

Background slits are calibrated with a number of pipeline steps, removing instrument signatures, and combined, then the combined array has to be un-calibrated (processing inverted) to be subtracted from the raw science.
created_at: 2025-04-09T18:16:12Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        science.var_flat = (science.data / flat_data * flat_err) ** 2
     else:
         science.data *= flat_data
+        science.var_flat = (science.data * flat_data / flat_err) ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
+        if not inverse:
+            science.var_poisson /= flat_data_squared
+            science.var_rnoise /= flat_data_squared
+        else:
+            science.var_poisson *= flat_data_squared
+            science.var_rnoise *= flat_data_squared
         science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
path: jwst/flatfield/flat_field.py
user: login: pllim
body: When the ERR math is generic like this, I don't have to inverse anything, right?
created_at: 2025-04-09T19:27:45Z
author_association: COLLABORATOR
diff_hunk: @@ -419,17 +427,20 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = (slit.data * slit_flat.data / slit_flat.err) ** 2
+            #np.place(slit.var_flat, np.isinf(slit.var_flat), np.nan)  # REMOVE?
path: jwst/flatfield/flat_field.py
user: login: pllim
body: Note to self: See if np.place is still needed after another RT. If not, remove from code.
created_at: 2025-04-09T19:28:18Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        science.var_flat = (science.data / flat_data * flat_err) ** 2
     else:
         science.data *= flat_data
+        science.var_flat = (science.data * flat_data / flat_err) ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
+        if not inverse:
+            science.var_poisson /= flat_data_squared
+            science.var_rnoise /= flat_data_squared
+        else:
+            science.var_poisson *= flat_data_squared
+            science.var_rnoise *= flat_data_squared
         science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: Yes, that should be fine forwards or backwards.
created_at: 2025-04-09T19:29:23Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +265,23 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flats = create_nirspec_flats(w_shape, flat_data_value=0.9)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], 1.371742, rtol=1e-6)  # FIXME: should be orig_data.slits[0].data[nn] / 0.9 ?
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: I am stuck designing a test for the inverse roundtrip. Would need a flatfield step expert to help here.
created_at: 2025-04-10T21:54:36Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +265,23 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flats = create_nirspec_flats(w_shape, flat_data_value=0.9)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], 1.371742, rtol=1e-6)  # FIXME: should be orig_data.slits[0].data[nn] / 0.9 ?
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: https://innerspace.stsci.edu/display/SCSB/JWST+calibration+pipeline+maintainers says @jemorrison @stscirij @melanieclarke ? 🙏 
created_at: 2025-04-11T14:00:35Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +265,23 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flats = create_nirspec_flats(w_shape, flat_data_value=0.9)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], 1.371742, rtol=1e-6)  # FIXME: should be orig_data.slits[0].data[nn] / 0.9 ?
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: NIRSpec flats are a complex combination of 3 different components (s, d, and f).  You might try adding a non-unitary value to just one of them, instead of all 3.
created_at: 2025-04-11T14:04:27Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +265,23 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flats = create_nirspec_flats(w_shape, flat_data_value=0.9)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], 1.371742, rtol=1e-6)  # FIXME: should be orig_data.slits[0].data[nn] / 0.9 ?
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: Got it. Thanks!
created_at: 2025-04-11T15:03:23Z
author_association: COLLABORATOR
diff_hunk: @@ -252,9 +251,9 @@ def test_nirspec_fs_flat(srctype):
     data.slits.append(datamodels.SlitModel(shape))
     data.slits[0].data = np.full(shape, 1.0)
     data.slits[0].dq = np.full(shape, 0)
-    data.slits[0].err = np.full(shape, 0.0)
-    data.slits[0].var_poisson = np.full(shape, 0.0)
-    data.slits[0].var_rnoise = np.full(shape, 0.0)
+    data.slits[0].err = np.full(shape, 0.1)
+    data.slits[0].var_poisson = np.full(shape, 0.01)
+    data.slits[0].var_rnoise = np.full(shape, 0.01)
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: I don't remember why I changed these. But then again probably unrealistic to assume 0 error/variance. Should I revert?
created_at: 2025-04-11T15:06:35Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        science.var_flat = (science.data / flat_data * flat_err) ** 2
     else:
         science.data *= flat_data
+        science.var_flat = (science.data * flat_data / flat_err) ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
+        if not inverse:
+            science.var_poisson /= flat_data_squared
+            science.var_rnoise /= flat_data_squared
+        else:
+            science.var_poisson *= flat_data_squared
+            science.var_rnoise *= flat_data_squared
         science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
     else:
-        flat_data_squared = flat_data ** 2
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
-
         # Set the output ERR to be the combined input ERR plus flatfield ERR, summed in quadrature
         science.err = np.sqrt(science.err**2 + science.var_flat)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: This error calculation for guider cal processing doesn't look right in either forwards or backwards processing.  Can you add a TODO note here? I can file a follow up ticket to get it fixed later.
created_at: 2025-04-14T16:51:38Z
author_association: COLLABORATOR
diff_hunk: @@ -252,9 +251,9 @@ def test_nirspec_fs_flat(srctype):
     data.slits.append(datamodels.SlitModel(shape))
     data.slits[0].data = np.full(shape, 1.0)
     data.slits[0].dq = np.full(shape, 0)
-    data.slits[0].err = np.full(shape, 0.0)
-    data.slits[0].var_poisson = np.full(shape, 0.0)
-    data.slits[0].var_rnoise = np.full(shape, 0.0)
+    data.slits[0].err = np.full(shape, 0.1)
+    data.slits[0].var_poisson = np.full(shape, 0.01)
+    data.slits[0].var_rnoise = np.full(shape, 0.01)
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: It should be fine to add error values here, as long as the tests are still passing.
created_at: 2025-04-14T17:05:34Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,28 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    # varflat and err not same as original input due to how they are calculated.
+    rt_varflat = (rt_data.slits[0].data[nn] * flat_val / flat_err) ** 2  # orig_input has no var_flat
+    rt_err = np.sqrt(rt_data.slits[0].var_poisson[nn] + rt_data.slits[0].var_rnoise[nn] + rt_varflat)
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: I'm really glad you wrote this test -- it hadn't occurred to me while looking over this before, but of course there is no var_flat before the flat step.  To inverse the processing, we should just be setting var_flat to None and remake the err array from the read noise and poisson arrays.
created_at: 2025-04-14T18:09:35Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,25 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        science.var_flat = (science.data / flat_data * flat_err) ** 2
     else:
         science.data *= flat_data
+        science.var_flat = (science.data * flat_data / flat_err) ** 2
 
     # Update the variances using BASELINE algorithm.  For guider data, it has
     # not gone through ramp fitting so there is no Poisson noise or readnoise
     if not isinstance(science, datamodels.GuiderCalModel):
-        flat_data_squared = flat_data ** 2
-        science.var_poisson /= flat_data_squared
-        science.var_rnoise /= flat_data_squared
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
+        if not inverse:
+            science.var_poisson /= flat_data_squared
+            science.var_rnoise /= flat_data_squared
+        else:
+            science.var_poisson *= flat_data_squared
+            science.var_rnoise *= flat_data_squared
         science.err = np.sqrt(science.var_poisson + science.var_rnoise + science.var_flat)
     else:
-        flat_data_squared = flat_data ** 2
-        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
-
         # Set the output ERR to be the combined input ERR plus flatfield ERR, summed in quadrature
         science.err = np.sqrt(science.err**2 + science.var_flat)
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: Here's the ticket to follow up on the error propagation for guider data:
https://jira.stsci.edu/browse/JP-3984
created_at: 2025-04-14T20:37:05Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,28 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    # varflat and err not same as original input due to how they are calculated.
+    rt_varflat = (rt_data.slits[0].data[nn] * flat_val / flat_err) ** 2  # orig_input has no var_flat
+    rt_err = np.sqrt(rt_data.slits[0].var_poisson[nn] + rt_data.slits[0].var_rnoise[nn] + rt_varflat)
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: OK thanks. Good catch! I set it to None on inverse step. However in the test, it does not come back as None, but all zeroes. Does that make sense to you?
created_at: 2025-04-15T12:43:47Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,28 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    # varflat and err not same as original input due to how they are calculated.
+    rt_varflat = (rt_data.slits[0].data[nn] * flat_val / flat_err) ** 2  # orig_input has no var_flat
+    rt_err = np.sqrt(rt_data.slits[0].var_poisson[nn] + rt_data.slits[0].var_rnoise[nn] + rt_varflat)
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: Yes, if you check for the data, the datamodel will automatically fill it in with the default value (0.0) and shape.  It's okay if var_flat is present and all zero after the inverse, rather than None.
created_at: 2025-04-15T13:06:01Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,27 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat, 0)  # orig_input has no var_flat
+    # err not same as original input due to how it is calculated.
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: I think the err should now be the same - can you check?
created_at: 2025-04-15T13:11:04Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,27 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat, 0)  # orig_input has no var_flat
+    # err not same as original input due to how it is calculated.
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: I did. It was not. If this must roundtrip back to input ERR, then I need to ponder the forward transform math a bit more.
created_at: 2025-04-15T13:13:29Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,27 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat, 0)  # orig_input has no var_flat
+    # err not same as original input due to how it is calculated.
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: Oh, it might just be because of how we set up the synthetic data - if the err array on input is not the sum in quadrature of the var components, then it won't match up when inversed.
created_at: 2025-04-15T13:17:47Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,27 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat, 0)  # orig_input has no var_flat
+    # err not same as original input due to how it is calculated.
path: jwst/flatfield/tests/test_flatfield.py
user: login: pllim
body: Ah yes, that makes sense. It is the same now after I fix the input ERR math. Thanks!
created_at: 2025-04-15T13:32:38Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,28 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        # NOTE: Re-arranging var_flat math will cause some NIRISS SOSS
+        # regression tests to fail with floating point differences.
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
path: jwst/flatfield/flat_field.py
user: login: pllim
body: I think I found the culprit for NIRISS failures. 😭 

RT running at https://github.com/spacetelescope/RegressionTests/actions/runs/14477166563
created_at: 2025-04-15T19:10:13Z
author_association: COLLABORATOR
diff_hunk: @@ -205,23 +206,28 @@ def apply_flat_field(science, flat, inverse=False):
 
     # Now let's apply the correction to science data and error arrays.  Rely
     # on array broadcasting to handle the cubes
+    flat_data_squared = flat_data * flat_data
     if not inverse:
         science.data /= flat_data
+        # NOTE: Re-arranging var_flat math will cause some NIRISS SOSS
+        # regression tests to fail with floating point differences.
+        science.var_flat = science.data ** 2 / flat_data_squared * flat_err ** 2
     else:
         science.data *= flat_data
+        science.var_flat = None  # Does not exist before flatfield step
path: jwst/flatfield/flat_field.py
user: login: melanieclarke
body: Instead of None, here and elsewhere:
```suggestion
        science.var_flat = np.zeros_like(science.data)  # Does not exist before flatfield step
```
created_at: 2025-04-15T20:47:06Z
author_association: COLLABORATOR
diff_hunk: @@ -265,13 +264,25 @@ def test_nirspec_fs_flat(srctype):
     data.slits[0].xsize = shape[1]
     data.slits[0].ysize = shape[0]
 
-    flats = create_nirspec_flats(w_shape)
+    orig_data = data.copy()
+    flat_val = 0.9
+    flat_err = 0.1
+    flats = create_nirspec_flats(w_shape, flat_data_value=flat_val, flat_err_value=flat_err)
     result = FlatFieldStep.call(data, override_fflat=flats[0], override_sflat=flats[1],
                                 override_dflat=flats[2], override_flat='N/A')
 
-    # null flat, so data is the same, other than nan edges
     nn = ~np.isnan(result.slits[0].data)
-    assert np.allclose(result.slits[0].data[nn], data.slits[0].data[nn])
+    assert_allclose(result.slits[0].data[nn], orig_data.slits[0].data[nn] / flat_val, rtol=1e-6)
+
+    # Recover original input before flatfield step.
+    rt_data = FlatFieldStep.call(
+        result, inverse=True, override_fflat=flats[0], override_sflat=flats[1],
+        override_dflat=flats[2], override_flat='N/A')
+    assert_allclose(rt_data.slits[0].data[nn], orig_data.slits[0].data[nn])
+    assert_allclose(rt_data.slits[0].var_poisson[nn], orig_data.slits[0].var_poisson[nn])
+    assert_allclose(rt_data.slits[0].var_rnoise[nn], orig_data.slits[0].var_rnoise[nn])
+    assert_allclose(rt_data.slits[0].var_flat, 0)  # orig_input has no var_flat
path: jwst/flatfield/tests/test_flatfield.py
user: login: melanieclarke
body: ```suggestion
    assert_allclose(rt_data.slits[0].var_flat[nn], 0)  # orig_input has no var_flat
```
created_at: 2025-04-15T20:55:53Z
author_association: COLLABORATOR
diff_hunk: @@ -419,18 +430,21 @@ def nirspec_fs_msa(output_model, f_flat_model, s_flat_model, d_flat_model, dispa
             flat_slits.append(slit_flat)
 
         # Now let's apply the correction to science data and error arrays.  Rely
-        # on array broadcasting to handle the cubes
+        # on array broadcasting to handle the cubes.
+        # Also update the variances using BASELINE algorithm.
+        flat_data_squared = slit_flat.data * slit_flat.data
         if not inverse:
             slit.data /= slit_flat.data
+            slit.var_poisson /= flat_data_squared
+            slit.var_rnoise /= flat_data_squared
+            slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
+            slit.err = np.sqrt(slit.var_poisson + slit.var_rnoise + slit.var_flat)
         else:
             slit.data *= slit_flat.data
-
-        # Update the variances using BASELINE algorithm
-        flat_data_squared = slit_flat.data ** 2
-        slit.var_poisson /= flat_data_squared
-        slit.var_rnoise /= flat_data_squared
-        slit.var_flat = (slit.data / slit_flat.data * slit_flat.err) ** 2
-        slit.err = np.sqrt(slit.var_poisson + slit.var_rnoise + slit.var_flat)
+            slit.var_poisson *= flat_data_squared
+            slit.var_rnoise *= flat_data_squared
+            slit.var_flat = None  # Does not exist before flatfield step
path: jwst/flatfield/flat_field.py
user: login: pllim
body: ```suggestion
            slit.var_flat = np.zeros_like(slit.data)  # Does not exist before flatfield step
```
created_at: 2025-04-15T21:02:52Z
author_association: COLLABORATOR
diff_hunk: @@ -498,21 +513,26 @@ def nirspec_brightobj(output_model, f_flat_model, s_flat_model, d_flat_model, di
             output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis
         )
 
+    # Update the variances and uncertainty array using BASELINE algorithm
+    flat_data_squared = interpolated_flat.data * interpolated_flat.data
     if not inverse:
         output_model.data /= interpolated_flat.data
+        output_model.var_poisson /= flat_data_squared
+        output_model.var_rnoise /= flat_data_squared
+        output_model.var_flat = (output_model.data / interpolated_flat.data * interpolated_flat.err) ** 2
+        output_model.err = np.sqrt(
+            output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
+        )
     else:
         output_model.data *= interpolated_flat.data
+        output_model.var_poisson *= flat_data_squared
+        output_model.var_rnoise *= flat_data_squared
+        output_model.var_flat = None  # Does not exist before flatfield step
path: jwst/flatfield/flat_field.py
user: login: pllim
body: ```suggestion
        output_model.var_flat = np.zeros_like(output_model.data)  # Does not exist before flatfield step
```
created_at: 2025-04-15T21:03:49Z
author_association: COLLABORATOR
diff_hunk: @@ -567,21 +588,26 @@ def nirspec_ifu(output_model, f_flat_model, s_flat_model, d_flat_model, dispaxis
         )
 
     if any_updated:
+        # Update the variances and uncertainty array using BASELINE algorithm
+        flat_data_squared = flat * flat
         if not inverse:
             output_model.data /= flat
+            output_model.var_poisson /= flat_data_squared
+            output_model.var_rnoise /= flat_data_squared
+            output_model.var_flat = (output_model.data / flat * flat_err) ** 2
+            output_model.err = np.sqrt(
+                output_model.var_poisson + output_model.var_rnoise + output_model.var_flat
+            )
         else:
             output_model.data *= flat
+            output_model.var_poisson *= flat_data_squared
+            output_model.var_rnoise *= flat_data_squared
+            output_model.var_flat = None  # Does not exist before flatfield step
path: jwst/flatfield/flat_field.py
user: login: pllim
body: ```suggestion
            output_model.var_flat = np.zeros_like(output_model.data)  # Does not exist before flatfield step
```
created_at: 2025-04-15T21:04:15Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-03T02:06:27Z
body: ## [Codecov](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?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 `62.13592%` with `39 lines` in your changes missing coverage. Please review.
> Project coverage is 75.37%. Comparing base [(`6d07df9`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/6d07df9ebe8570f8bbbd1e143bbda29c178d6fd9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) to head [(`e06c0f1`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/e06c0f14cddd8126266e7c5817aa905d72d41d52?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).
> Report is 665 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?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/photom/photom.py](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?src=pr&el=tree&filepath=jwst%2Fphotom%2Fphotom.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-andzdC9waG90b20vcGhvdG9tLnB5) | 67.69% | [21 Missing :warning: ](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) |
| [jwst/flatfield/flat\_field.py](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?src=pr&el=tree&filepath=jwst%2Fflatfield%2Fflat_field.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-andzdC9mbGF0ZmllbGQvZmxhdF9maWVsZC5weQ==) | 52.63% | [18 Missing :warning: ](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?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    #9356      +/-   ##
==========================================
- Coverage   75.60%   75.37%   -0.24%     
==========================================
  Files         368      368              
  Lines       36855    36883      +28     
==========================================
- Hits        27865    27800      -65     
- Misses       8990     9083      +93     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/spacetelescope/jwst/pull/9356?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: melanieclarke
created_at: 2025-04-03T13:17:03Z
body: I have not reviewed in detail, but changes look good to me so far.

The 4 steps that use inverse as an input parameter are:  photom, barshadow, pathloss, and flat_field.  Barshadow and pathloss are addressed elsewhere, but it looks like flat_field also needs fixing.  Can you address that one here too?
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-03T15:26:31Z
body: Sorry about all the unrelated fixes. I told myself to be good and only do the actual fix but then I was just too triggered.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-04T01:43:03Z
body: I also made changes to flatfield. There are actual differences seen in RT but I cannot tell if they are expected or not: https://github.com/spacetelescope/RegressionTests/actions/runs/14252934458/job/39949867173

SCI changed too but not by much and not a lot of pixels. I am guessing there are floating point differences between `data /= factor` and `data *= precomputed_inverse_factor`. 🤷‍♀️ 
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-04T14:24:55Z
body: > I also made changes to flatfield. There are actual differences seen in RT but I cannot tell if they are expected or not: https://github.com/spacetelescope/RegressionTests/actions/runs/14252934458/job/39949867173
> 
> SCI changed too but not by much and not a lot of pixels. I am guessing there are floating point differences between `data /= factor` and `data *= precomputed_inverse_factor`. 🤷‍♀️

That is more changes than I would have expected.  I agree they're probably not scientifically significant, but we'll need to look through them.  Can you try running a couple cases locally and see if `data /= factor` restores the truth values, for comparison?

This one definitely looked problematic though: `test_nirspec_mos_mbkg[masterbg2d]: jwst.regtest.test_nirspec_masterbackground`
There are some new `inf` values in the VAR_FLAT and ERR extensions.  This is the one place where the inverted error propagations will show up in the products, so probably we need to check for some edge cases in the new variance handling.  NIRSpec flat variance is prone to floating point overflow.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-04T16:39:20Z
body: > check for some edge cases in the new variance handling

Is there a standard way for such checks in this package I can refer to as example implementation? For instance, what do I do when a factor is zero and inversing it becomes INF? Do I also have to set DQ flag?
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-04T17:05:10Z
body: > Is there a standard way for such checks in this package I can refer to as example implementation? For instance, what do I do when a factor is zero and inversing it becomes INF? Do I also have to set DQ flag?

It depends on the operation.  If we can avoid overflow with a better order of operations, we should.  We also try to avoid inf if possible, by handling divide by zero cases separately -- NaN is often a more acceptable value than inf.

Here's an example from the flat_field step for handling both:
https://github.com/spacetelescope/jwst/blob/21bd3589d60dee1cc084d19ba60d4fb6a060fc16/jwst/flatfield/flat_field.py#L672
Since that's the module you're working in, maybe start with that as a model.

For DQ handling, generally we match NaN values across SCI, ERR, and VAR arrays and also set DQ = DO_NOT_USE.  This is handled by a util function:
https://github.com/spacetelescope/jwst/blob/21bd3589d60dee1cc084d19ba60d4fb6a060fc16/jwst/lib/pipe_utils.py#L89
It ought to be called at the end of any step that creates new NaNs, so you shouldn't need to explicitly worry about it in the processing, but it's a good idea to check.  For flat_field, there is a unit test to verify in the forward processing:
https://github.com/spacetelescope/jwst/blob/21bd3589d60dee1cc084d19ba60d4fb6a060fc16/jwst/flatfield/tests/test_flatfield.py#L67-L73
But we should probably add one for the inverse processing as well for verification. 

There are also a couple regtests that check this, e.g.:
https://github.com/spacetelescope/jwst/blob/21bd3589d60dee1cc084d19ba60d4fb6a060fc16/jwst/regtest/test_nirspec_steps_spec2.py#L61
If they don't cover the code you're modifying, we should make sure to write one that does.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-04T18:14:24Z
body: Thanks for the input! I might not get back to this until next week.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-09T20:33:18Z
body: I think the INF is gone. Should I wait for some "okify" to go through first to make sure any RT failures are actually from this PR?
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-09T20:37:23Z
body: > I think the INF is gone. Should I wait for some "okify" to go through first to make sure any RT failures are actually from this PR?

I think you can go ahead and run them. I don't know of any outstanding failures in RT right now, except the 1 SOSS failure in devdeps.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-09T22:33:21Z
body: RT has 5 failures; same 5 for stable and devdeps. Who should review the diff? If these are okay, I will remove the commented out `np.place` hack and take this out of draft. Thanks for your patience!

![Screenshot 2025-04-09 183202](https://github.com/user-attachments/assets/2f9b7a06-6c1f-44e7-9f0e-bc69da2dbf81)

author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-10T13:53:06Z
body: > RT has 5 failures; same 5 for stable and devdeps. Who should review the diff? If these are okay, I will remove the commented out `np.place` hack and take this out of draft. Thanks for your patience!

The changes to the error and variance in the masterbg2d product for MOS are the only changes I would expect to see.  The SOSS product changes are confusing -- I don't think they should be there.  When we are able to run RT on main again, we should check if there are diffs on main for those files.
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-10T14:08:34Z
body: Can you please add some unit tests for inversing photom and flat_field?  We should check that the original data, error, and variance are recovered after inverse on processed data.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-10T14:16:55Z
body: Sounds good. I will do that when I get back to this PR. Thanks!
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-10T19:59:30Z
body: I was hoping I could hook into existing flatfield tests but turns out all the test data have unity flat, which would defeat the purpose of inverse. I will have to ponder writing a completely new test for this.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-11T21:35:34Z
body: https://github.com/spacetelescope/RegressionTests/actions/runs/14406105552/attempts/1#summary-40403084134

SOSS failures still there but differences seem small enough? The masterbg one will need some expert eyes.

Thanks!
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-15T13:26:37Z
body: I am pondering the NIRISS SOSS regtest errors. It makes no sense on why variance math change would affect EXTRACT1D FLUX col. I am guessing it comes from `apply_flat_field` function (rather than the other `nirspec` ones that I touched) but as suspected, changing the squared variable from `x * x` back to `x ** 2` does not fix the failure.
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-15T13:32:42Z
body: > I am pondering the NIRISS SOSS regtest errors. It makes no sense on why variance math change would affect EXTRACT1D FLUX col. I am guessing it comes from `apply_flat_field` function (rather than the other `nirspec` ones that I touched) but as suspected, changing the squared variable from `x * x` back to `x ** 2` does not fix the failure.

Can you check the photom change?  NIRISS SOSS calibrates flux after extraction, unlike the other instruments/modes.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-15T14:05:03Z
body: Reverting all the `x * x` to `x ** 2` in photom didn't make any difference either. I think I need to start a new branch and reapply the changes bit by bit and see, because it is passing on main. The tests are very slow, so it might be a while before I can report back. Thank you for your patience.
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-15T14:12:13Z
body: > The tests are very slow, so it might be a while before I can report back. Thank you for your patience.

No rush!  Thanks for your diligence.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-15T19:58:42Z
body: RT only has 1 failure now: `jwst.regtest.test_nirspec_masterbackground::test_nirspec_mos_mbkg[masterbg2d]`
author_association: COLLABORATOR
user: login: melanieclarke
created_at: 2025-04-15T20:10:54Z
body: > RT only has 1 failure now: `jwst.regtest.test_nirspec_masterbackground::test_nirspec_mos_mbkg[masterbg2d]`

Great, that's the only one expected.  I'll take a look at it.
author_association: COLLABORATOR
user: login: pllim
created_at: 2025-04-15T21:18:21Z
body: I was wondering about the FITSDiff complaining about VAR_FLAT dimensions. Thanks for the suggestions! I think I have addressed your comments and also rerunning RT again at https://github.com/spacetelescope/RegressionTests/actions/runs/14479420182
author_association: COLLABORATOR