Repository: spacetelescope/jwst
Pull request #9390: JP-3861: Persistence style updates
Author: login: stscirij
Labels: name: persistence
name: testing
name: no-changelog-entry-needed
Created: 2025-04-10T15:04:24Z | Merged: 2025-05-07T20:11:24Z

<!-- If this PR closes a JIRA ticket, make sure the title starts with the JIRA issue number,
for example JP-1234: <Fix a bug> -->
Addresses [JP-3861](https://jira.stsci.edu/browse/JP-3861)

<!-- 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 implements style changes for the persistence step

<!-- 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`)
  - [ ] 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
  - [ ] update relevant docstrings and / or `docs/` page
  - [ ] [start a regression test](https://github.com/spacetelescope/RegressionTests/actions/workflows/jwst.yml) and include a link to the running job ([click here for instructions](https://github.com/spacetelescope/RegressionTests/blob/main/docs/running_regression_tests.md))
    - [ ] Do truth files need to be updated ("okified")?
      - [ ] **after the reviewer has approved these changes**, run `okify_regtests` to update the truth files
- [ ] if a JIRA ticket exists, [make sure it is resolved properly](https://github.com/spacetelescope/jwst/wiki/How-to-resolve-JIRA-issues)



Diff:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 301d2091eb0..3f4152997bb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -43,7 +43,6 @@ repos:
             jwst/fringe/.* |
             jwst/ipc/.* |
             jwst/mrs_imatch/.* |
-            jwst/persistence/.* |
             jwst/photom/.* |
             jwst/tests/.* |
             docs/.* |
diff --git a/.ruff.toml b/.ruff.toml
index fefb92df789..59d47043cf8 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -25,7 +25,6 @@ exclude = [
     "jwst/fringe/**.py",
     "jwst/ipc/**.py",
     "jwst/mrs_imatch/**.py",
-    "jwst/persistence/**.py",
     "jwst/photom/**.py",
 ]
 
@@ -105,5 +104,4 @@ ignore-fully-untyped = true  # Turn off annotation checking for fully untyped co
 "jwst/fringe/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "jwst/ipc/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "jwst/mrs_imatch/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
-"jwst/persistence/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
 "jwst/photom/**.py" = ["D", "N", "A", "ARG", "B", "C4", "ICN", "INP", "ISC", "LOG", "NPY", "PGH", "PTH", "S", "SLF", "SLOT", "T20", "TRY", "UP", "YTT", "E501"]
diff --git a/jwst/persistence/__init__.py b/jwst/persistence/__init__.py
index a0a1fb8a074..0ce2e3483d4 100644
--- a/jwst/persistence/__init__.py
+++ b/jwst/persistence/__init__.py
@@ -1,3 +1,5 @@
+"""Remove persistence signal from ramp data."""
+
 from .persistence_step import PersistenceStep
 
-__all__ = ['PersistenceStep']
+__all__ = ["PersistenceStep"]
diff --git a/jwst/persistence/persistence.py b/jwst/persistence/persistence.py
index e2e43546d53..ac0460588b4 100644
--- a/jwst/persistence/persistence.py
+++ b/jwst/persistence/persistence.py
@@ -11,15 +11,20 @@
 log = logging.getLogger(__name__)
 log.setLevel(logging.DEBUG)
 
-SCALEFACTOR = 2.
+SCALEFACTOR = 2.0
 """This factor is to account for the difference in gain for charges freed
 from traps, compared with photon-generated charges.
 """
 
 
-def no_NaN(input_model, fill_value,
-           zap_nan=False, zap_zero=False):
-    """Replace NaNs and/or zeros with a fill value.
+def no_nan(
+    input_model,
+    fill_value,
+    zap_nan=False,
+    zap_zero=False,
+):
+    """
+    Replace NaNs and/or zeros with a fill value.
 
     Parameters
     ----------
@@ -32,7 +37,6 @@ def no_NaN(input_model, fill_value,
     zap_nan : bool
         If True, replace NaNs in a copy of `input_model.data`.
         The default is False.
-
     zap_zero : bool
         If True, replace zeros in a copy of `input_model.data`.
         The default is False.
@@ -43,17 +47,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = input_model.data == 0.0
         else:
-            mask = np.logical_or(mask, (input_model.data == 0.))
+            mask = np.logical_or(mask, (input_model.data == 0.0))
 
     if mask is None or mask.sum(dtype=np.intp) == 0:
         return input_model
@@ -63,8 +66,9 @@ def no_NaN(input_model, fill_value,
         return temp
 
 
-class DataSet():
-    """Input dataset to which persistence will be applied
+class DataSet:
+    """
+    Input dataset to which persistence will be applied.
 
     Attributes
     ----------
@@ -115,16 +119,23 @@ class DataSet():
         The number of resets (frames) at the beginning of each integration.
     """
 
-    def __init__(self, output_obj, input_traps_filled,
-                 flag_pers_cutoff, save_persistence,
-                 trap_density_model, trappars_model,
-                 persat_model):
-        """Assign values to attributes.
+    def __init__(
+        self,
+        output_obj,
+        input_traps_filled,
+        flag_pers_cutoff,
+        save_persistence,
+        trap_density_model,
+        trappars_model,
+        persat_model,
+    ):
+        """
+        Assign values to attributes.
 
         Parameters
         ----------
         output_obj : JWST data model
-            copy of input data model object
+            Copy of input data model object
 
         input_traps_filled : cube model or None
             Image of trap state.  There will be one or more image planes,
@@ -151,7 +162,6 @@ def __init__(self, output_obj, input_traps_filled,
         persat_model : persistence saturation model
             Persistence saturation limit (full well) reference file.
         """
-
         log.debug("input_traps_filled = %s", str(input_traps_filled))
         log.debug("flag_pers_cutoff = %g", flag_pers_cutoff)
         log.debug("save_persistence = %s", str(save_persistence))
@@ -167,15 +177,16 @@ def __init__(self, output_obj, input_traps_filled,
         self.persistencesat = persat_model
 
         # These will be populated from metadata.
-        self.tframe = 0.
-        self.tgroup = 0.
+        self.tframe = 0.0
+        self.tgroup = 0.0
         self.ngroups = 0
         self.nframes = 0
         self.groupgap = 0
         self.nresets = 0
 
     def do_all(self):
-        """Execute all tasks for persistence correction
+        """
+        Execute all tasks for persistence correction.
 
         Returns
         -------
@@ -195,7 +206,6 @@ def do_all(self):
         skipped : bool
             This will be True if the step has been skipped.
         """
-
         # Initial value, indicates that processing was done successfully.
         skipped = False
 
@@ -221,7 +231,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny) or (shape[-1] != det_nx)
         if is_subarray:
             log.debug("The input is a subarray.")
         else:
@@ -230,8 +240,7 @@ def do_all(self):
         if not self.traps_filled:
             have_traps_filled = False
             self.traps_filled = datamodels.TrapsFilledModel(
-                data=np.zeros((nfamilies, det_ny, det_nx),
-                              dtype=self.output_obj.data.dtype)
+                data=np.zeros((nfamilies, det_ny, det_nx), dtype=self.output_obj.data.dtype)
             )
             self.traps_filled.meta.subarray.xstart = 1
             self.traps_filled.meta.subarray.ystart = 1
@@ -239,12 +248,12 @@ def do_all(self):
             self.traps_filled.meta.subarray.ysize = det_ny
         else:
             have_traps_filled = True
-            self.traps_filled = no_NaN(self.traps_filled, 0., zap_nan=True)
+            self.traps_filled = no_nan(self.traps_filled, 0.0, zap_nan=True)
 
-        self.trap_density = no_NaN(self.trap_density, 0., zap_nan=True)
-        self.persistencesat = no_NaN(self.persistencesat, 1.e7, zap_nan=True)
+        self.trap_density = no_nan(self.trap_density, 0.0, zap_nan=True)
+        self.persistencesat = no_nan(self.persistencesat, 1.0e7, zap_nan=True)
 
-        if have_traps_filled:   # was there an actual traps_filled file?
+        if have_traps_filled:  # was there an actual traps_filled file?
             # Decrease traps_filled by the number of traps that decayed
             # in the time (to_start) from the end of the traps_filled file
             # to the start of the current exposure.
@@ -255,14 +264,13 @@ def do_all(self):
             # Decays during the time covered by the reset (again, if any)
             # between integrations will be taken care of in the loop
             # over integrations.
-            to_start = (self.output_obj.meta.exposure.start_time
-                        - self.traps_filled.meta.exposure.end_time) * 86400.
-            log.debug("Decay time for previous traps-filled file = %g s",
-                      to_start)
+            mjd_start = self.output_obj.meta.exposure.start_time
+            mjd_end = self.traps_filled.meta.exposure.end_time
+            to_start = (mjd_start - mjd_end) * 86400.0
+            log.debug("Decay time for previous traps-filled file = %g s", to_start)
             for k in range(nfamilies):
                 decay_param_k = self.get_decay_param(par, k)
-                decay = self.compute_decay(self.traps_filled.data[k],
-                                           decay_param_k, to_start)
+                decay = self.compute_decay(self.traps_filled.data[k], decay_param_k, to_start)
                 self.traps_filled.data[k, :, :] -= decay
                 del decay
 
@@ -283,7 +291,6 @@ def do_all(self):
             filled
             cr_filled
         """
-
         # If the science image is a subarray, extract matching sections of
         # reference images unless they match the science image.
 
@@ -302,9 +309,9 @@ def do_all(self):
         # file are flagged as bad, set the corresponding data values to
         # zero, so the computed number of traps will also be zero.
         if hasattr(self.trap_density, "dq"):
-            mask = (np.bitwise_and(self.trap_density.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
-            self.trap_density.data[mask] = 0.
+            do_not_use_flag = np.bitwise_and(self.trap_density.dq, dqflags.pixel["DO_NOT_USE"])
+            mask = np.where(do_not_use_flag > 0)
+            self.trap_density.data[mask] = 0.0
             del mask
 
         slc = self.get_slice(self.persistencesat, self.output_obj)
@@ -314,7 +321,7 @@ def do_all(self):
         # Initialize the optional output.
         if self.save_persistence:
             self.output_pers = self.output_obj.copy()
-            self.output_pers.data[:, :, :, :] = 0.
+            self.output_pers.data[:, :, :, :] = 0.0
             self.output_pers.pixeldq = None
             self.output_pers.groupdq = None
             self.output_pers.err = None
@@ -327,16 +334,16 @@ def do_all(self):
 
         # self.traps_filled will be updated with each integration, to
         # account for charge capture and decay of traps.
-        filled = -1                             # just to ensure that it exists
+        filled = -1  # just to ensure that it exists
         for integ in range(nints):
-            self.get_group_info(integ)          # self.tgroup, etc.
-            decayed[:, :, :] = 0.               # initialize
+            self.get_group_info(integ)  # self.tgroup, etc.
+            decayed[:, :, :] = 0.0  # initialize
             # slope has to be computed early in the loop over integrations,
             # before the data are modified by subtracting persistence.
             # The slope is needed for computing charge captures.
             (grp_slope, slope) = self.compute_slope(integ)
             for group in range(ngroups):
-                persistence[:, :] = 0.          # initialize
+                persistence[:, :] = 0.0  # initialize
                 for k in range(nfamilies):
                     decay_param_k = self.get_decay_param(par, k)
                     # Compute and subtract the decays during the reset.
@@ -344,14 +351,14 @@ def do_all(self):
                     # first integration have already been accounted for.
                     if integ > 0 and group == 0 and self.nresets > 0:
                         reset_time = self.tframe * self.nresets
-                        decay_during_reset = \
-                            self.compute_decay(self.traps_filled.data[k],
-                                               decay_param_k, reset_time)
+                        decay_during_reset = self.compute_decay(
+                            self.traps_filled.data[k], decay_param_k, reset_time
+                        )
                         self.traps_filled.data[k, :, :] -= decay_during_reset
                     # Decays during current group, for current trap family.
-                    decayed_in_group = \
-                        self.compute_decay(self.traps_filled.data[k],
-                                           decay_param_k, t_group)
+                    decayed_in_group = self.compute_decay(
+                        self.traps_filled.data[k], decay_param_k, t_group
+                    )
                     # Cumulative decay to the end of the current group.
                     decayed[k, :, :] += decayed_in_group
                     self.traps_filled.data[k, :, :] -= decayed_in_group
@@ -364,23 +371,21 @@ def do_all(self):
                 # Persistence was computed in DN.
                 self.output_obj.data[integ, group, :, :] -= persistence
                 if self.save_persistence:
-                    self.output_pers.data[integ, group, :, :] = \
-                        persistence.copy()
+                    self.output_pers.data[integ, group, :, :] = persistence.copy()
                 if persistence.max() >= self.flag_pers_cutoff:
-                    mask = (persistence >= self.flag_pers_cutoff)
-                    self.output_obj.pixeldq[mask] |= dqflags.pixel['PERSISTENCE']
+                    mask = np.where(persistence >= self.flag_pers_cutoff)
+                    self.output_obj.pixeldq[mask] |= dqflags.pixel["PERSISTENCE"]
 
             # Update traps_filled with the number of traps that captured
             # a charge during the current integration.
             for k in range(nfamilies):
                 capture_param_k = self.get_capture_param(par, k)
                 # This may be a subarray.
-                filled = self.predict_capture(capture_param_k,
-                                              self.trap_density.data,
-                                              integ, grp_slope, slope)
+                filled = self.predict_capture(
+                    capture_param_k, self.trap_density.data, integ, grp_slope, slope
+                )
                 if is_subarray:
-                    self.traps_filled.data[k, save_slice[0],
-                                           save_slice[1]] += filled
+                    self.traps_filled.data[k, save_slice[0], save_slice[1]] += filled
                 else:
                     self.traps_filled.data[k, :, :] += filled
 
@@ -401,7 +406,8 @@ def do_all(self):
         return self.output_obj, self.traps_filled, self.output_pers, skipped
 
     def get_slice(self, ref, sci):
-        """Find the 2-D slice for a reference file.
+        """
+        Find the 2-D slice for a reference file.
 
         Parameters
         ----------
@@ -417,7 +423,6 @@ def get_slice(self, ref, sci):
             The elements are the Y and X slices that are intended to be
             used to extract a subarray from the reference file.
         """
-
         sci_shape = sci.shape
         sci_nx = sci_shape[-1]
         sci_ny = sci_shape[-2]
@@ -440,16 +445,15 @@ def get_slice(self, ref, sci):
         ystop = ystart + sci_ny
 
         # Check for errors in the slice indexes
-        if (xstart < 0 or ystart < 0 or
-                xstop > ref.data.shape[-1] or ystop > ref.data.shape[-2]):
+        if xstart < 0 or ystart < 0 or xstop > ref.data.shape[-1] or ystop > ref.data.shape[-2]:
             log.error("Science and reference file arrays not compatible")
-            raise ValueError("Can't extract matching subarray from "
-                             "reference data")
+            raise ValueError("Can't extract matching subarray from reference data")
 
         return slice(ystart, ystop), slice(xstart, xstop)
 
     def ref_matches_sci(self, ref, slc):
-        """Test whether ref and sci cover the same area of the detector.
+        """
+        Test whether ref and sci cover the same area of the detector.
 
         Parameters
         ----------
@@ -466,7 +470,6 @@ def ref_matches_sci(self, ref, slc):
             True if both are full-frame or if they are the same subarray;
             False otherwise.
         """
-
         slc_test_y = slice(0, ref.shape[-2])
         slc_test_x = slice(0, ref.shape[-1])
 
@@ -476,7 +479,8 @@ def ref_matches_sci(self, ref, slc):
             return False
 
     def get_subarray(self, ref, slc):
-        """Extract a subarray from a reference file.
+        """
+        Extract a subarray from a reference file.
 
         Parameters
         ----------
@@ -493,7 +497,6 @@ def get_subarray(self, ref, slc):
             `refsub` is a copy of `ref`, but the data attribute in `refsub`
             includes only the relevant slice.
         """
-
         refsub = ref.copy()
         # If the reference file data might have a dimension greater than
         # two, use this syntax:
@@ -505,7 +508,8 @@ def get_subarray(self, ref, slc):
         return refsub
 
     def get_parameters(self):
-        """Read capture and decay parameters from a reference table.
+        """
+        Read capture and decay parameters from a reference table.
 
         Returns
         -------
@@ -521,7 +525,6 @@ def get_parameters(self):
         par3 : ndarray
             Column "decay_param" from the trappars table.
         """
-
         data = self.trappars_model.trappars_table
         par0 = data["capture0"].copy()
         par1 = data["capture1"].copy()
@@ -531,10 +534,9 @@ def get_parameters(self):
         return par0, par1, par2, par3
 
     def compute_slope(self, integ):
-        """Compute an estimate of the slope of the ramp for each pixel.
+        """
+        Compute an estimate of the slope of the ramp for each pixel.
 
-        Extended Summary
-        ----------------
         We need a value for the slope that will not include cosmic-ray
         jumps, and groups that are flagged as saturated must also not
         contribute to the slope.  The approach will be to find the
@@ -567,14 +569,13 @@ def compute_slope(self, integ):
             The ramp slope in units of fraction of the persistence
             saturation limit per second.
         """
-
         (_, ngroups, ny, nx) = self.output_obj.shape
         if ngroups == 1:
             # This won't be accurate, because there's only one group.
             grp_slope = self.output_obj.data[integ, 0, :, :]
             if hasattr(self.persistencesat, "dq"):
-                mask = (np.bitwise_and(self.persistencesat.dq,
-                                       dqflags.pixel["DO_NOT_USE"]) > 0)
+                do_not_use = np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"])
+                mask = do_not_use > 0
                 if mask.sum() == 0:
                     mask = None
             else:
@@ -583,9 +584,8 @@ def compute_slope(self, integ):
                 slope = grp_slope / (self.persistencesat.data * self.tgroup)
             else:
                 # Set to 1 so we don't divide by 0.
-                persat = np.where(mask, 1., self.persistencesat.data)
-                slope = np.where(mask, 0.,
-                                 grp_slope / (persat * self.tgroup))
+                persat = np.where(mask, 1.0, self.persistencesat.data)
+                slope = np.where(mask, 0.0, grp_slope / (persat * self.tgroup))
             return grp_slope, slope
 
         gdqflags = dqflags.group
@@ -605,12 +605,12 @@ def compute_slope(self, integ):
         diff = data[1:, :, :] - data[0:-1, :, :]
         max_diff = diff.max()
         # Larger than any actual data value
-        huge_diff = max(2. * max_diff, 1.e5)
+        huge_diff = max(2.0 * max_diff, 1.0e5)
         del max_diff
 
         # This assumes that the saturation step has already been run.
         # n_sat is a 2-D array of the number of saturated groups per pixel.
-        s_mask = (np.bitwise_and(gdq[1:, :, :], gdqflags["SATURATED"]) > 0)
+        s_mask = np.bitwise_and(gdq[1:, :, :], gdqflags["SATURATED"]) > 0
         diff[s_mask] = huge_diff
         sat = s_mask.astype(np.int32)
         n_sat = sat.sum(axis=0)
@@ -620,27 +620,26 @@ def compute_slope(self, integ):
         # Upper limit of good data (slice notation, i.e. exclusive).
         upper = diff.shape[0] - (n_cr + n_sat)
         really_bad = np.where(upper < 0)
-        upper[really_bad] = 0           # for the comparison with indx below
+        upper[really_bad] = 0  # for the comparison with indx below
         del really_bad
         temp = np.arange(ngroups - 1, dtype=np.int32)
         temp2 = np.repeat(temp, nx * ny)
         indx = temp2.reshape((ngroups - 1, ny, nx))
         mask = np.where(indx >= upper)
-        sdiff[mask] = 0.                # zero values won't affect the sum
+        sdiff[mask] = 0.0  # zero values won't affect the sum
         del temp, temp2, indx, mask
         bad = np.where(upper <= 0)
-        upper[bad] = 1                  # so we can divide by upper
+        upper[bad] = 1  # so we can divide by upper
 
         # grp_slope has units of DN / group
         grp_slope = np.sum(sdiff, axis=0) / upper.astype(np.float32)
-        grp_slope[bad] = 0.
+        grp_slope[bad] = 0.0
         del bad
 
         # slope will have units (DN / persistence_saturation_limit) / second,
         # where persistence_saturation_limit is in units of DN.
         if hasattr(self.persistencesat, "dq"):
-            mask = (np.bitwise_and(self.persistencesat.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
+            mask = np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
             if mask.sum() == 0:
                 mask = None
         else:
@@ -649,14 +648,14 @@ def compute_slope(self, integ):
             slope = grp_slope / (self.persistencesat.data * self.tgroup)
         else:
             # Set to 1 so we don't divide by 0.
-            persat = np.where(mask, 1., self.persistencesat.data)
-            slope = np.where(mask, 0.,
-                             grp_slope / (persat * self.tgroup))
+            persat = np.where(mask, 1.0, self.persistencesat.data)
+            slope = np.where(mask, 0.0, grp_slope / (persat * self.tgroup))
 
         return grp_slope, slope
 
     def get_capture_param(self, par, k):
-        """Extract capture parameters for the current trap family.
+        """
+        Extract capture parameters for the current trap family.
 
         Parameters
         ----------
@@ -673,13 +672,13 @@ def get_capture_param(self, par, k):
         tuple of three floats
             These are the capture parameters for the current trap family.
         """
-
         (par0, par1, par2) = par[0:3]
 
         return par0[k], par1[k], par2[k]
 
     def get_decay_param(self, par, k):
-        """Extract decay parameter(s) for the current trap family.
+        """
+        Extract decay parameter(s) for the current trap family.
 
         Parameters
         ----------
@@ -696,16 +695,14 @@ def get_decay_param(self, par, k):
         float
             This is the decay parameter for the current trap family.
         """
-
         par3 = par[3]
 
         return par3[k]
 
     def get_group_info(self, integ):
-        """Get some metadata.
+        """
+        Get some metadata.
 
-        Extended Summary
-        ----------------
         This method populates these attributes:
             self.tframe
             self.tgroup
@@ -721,7 +718,6 @@ def get_group_info(self, integ):
             resets before the first integration can be different from the
             number of resets between integrations.
         """
-
         shape = self.output_obj.data.shape
 
         self.tframe = self.output_obj.meta.exposure.frame_time
@@ -729,8 +725,7 @@ def get_group_info(self, integ):
         ngroups = self.output_obj.meta.exposure.ngroups
         if ngroups != shape[-3]:
             log.warning("model.meta and data disagree about ngroups:")
-            log.warning("  %d vs %d; will use %d.",
-                        ngroups, shape[-3], shape[-3])
+            log.warning("  %d vs %d; will use %d.", ngroups, shape[-3], shape[-3])
         self.ngroups = shape[-3]
         self.nframes = self.output_obj.meta.exposure.nframes
         self.groupgap = self.output_obj.meta.exposure.groupgap
@@ -745,9 +740,9 @@ def get_group_info(self, integ):
             else:
                 self.nresets = 1
 
-    def predict_capture(self, capture_param_k, trap_density, integ,
-                        grp_slope, slope):
-        """Compute the number of traps that will be filled in time dt.
+    def predict_capture(self, capture_param_k, trap_density, integ, grp_slope, slope):
+        """
+        Compute the number of traps that will be filled in time dt.
 
         This is based on Michael Regan's trapcapturemodel.pro.
 
@@ -782,7 +777,6 @@ def predict_capture(self, capture_param_k, trap_density, integ,
         ndarray, 2-D
             The computed traps_filled at the end of the integration.
         """
-
         data = self.output_obj.data[integ, :, :, :]
 
         t_frame = self.tframe
@@ -796,10 +790,9 @@ def predict_capture(self, capture_param_k, trap_density, integ,
         totaltime = ngroups * t_group + nresets * t_frame
 
         # Find pixels exceeding the persistence saturation limit (full well).
-        pflag = (data > self.persistencesat.data)
+        pflag = data > self.persistencesat.data
         if hasattr(self.persistencesat, "dq"):
-            mask = (np.bitwise_and(self.persistencesat.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
+            mask = np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
             mshape = mask.shape
             mask = mask.reshape((1,) + mshape)
             m3 = np.repeat(mask, ngroups, 0)
@@ -813,12 +806,10 @@ def predict_capture(self, capture_param_k, trap_density, integ,
         dt = totaltime - sattime
 
         # Traps that were filled due to the linear portion of the ramp.
-        ramp_traps_filled = self.predict_ramp_capture(
-            capture_param_k,
-            trap_density, slope, dt)
+        ramp_traps_filled = self.predict_ramp_capture(capture_param_k, trap_density, slope, dt)
 
         filled = ramp_traps_filled.copy()
-        mask = (sat_count > 0)
+        mask = sat_count > 0
         any_saturated = np.any(mask)
         if any_saturated:
             # Traps that were filled due to the saturated portion of the ramp.
@@ -826,20 +817,23 @@ def predict_capture(self, capture_param_k, trap_density, integ,
                 capture_param_k,
                 trap_density[mask],
                 ramp_traps_filled[mask],
-                sattime[mask], sat_count[mask], ngroups)
+                sattime[mask],
+                sat_count[mask],
+                ngroups,
+            )
         del sat_count, sattime, ramp_traps_filled, mask
 
         # Traps that were filled due to cosmic-ray jumps.
         cr_filled = self.delta_fcn_capture(
-            capture_param_k,
-            trap_density, integ,
-            grp_slope, ngroups, t_group)
+            capture_param_k, trap_density, integ, grp_slope, ngroups, t_group
+        )
         filled += cr_filled
 
         return filled
 
     def predict_ramp_capture(self, capture_param_k, trap_density, slope, dt):
-        """Compute the number of traps that will be filled in time dt.
+        """
+        Compute the number of traps that will be filled in time dt.
 
         This is based on Michael Regan's predictrampcapture3.pro.
 
@@ -870,32 +864,34 @@ def predict_ramp_capture(self, capture_param_k, trap_density, slope, dt):
         ndarray, 2-D
             The computed traps_filled at the end of the integration.
         """
-
         (par0, par1, par2) = capture_param_k
         if par1 == 0:
-            log.error("Capture parameter is zero; parameters are %g, %g, %g",
-                      par0, par1, par2)
-            tau = 1.e10                 # arbitrary "big" number
+            log.error("Capture parameter is zero; parameters are %g, %g, %g", par0, par1, par2)
+            tau = 1.0e10  # arbitrary "big" number
         else:
-            tau = 1. / abs(par1)
-
-        traps_filled = (trap_density * slope**2
-                        * (dt**2 * (par0 + par2) / 2.
-                           + par0 * (dt * tau + tau**2) * np.exp(-dt / tau)
-                           - par0 * tau**2))
+            tau = 1.0 / abs(par1)
+
+        traps_filled = (
+            trap_density
+            * slope**2
+            * (
+                dt**2 * (par0 + par2) / 2.0
+                + par0 * (dt * tau + tau**2) * np.exp(-dt / tau)
+                - par0 * tau**2
+            )
+        )
 
         traps_filled *= SCALEFACTOR
         return traps_filled
 
-    def predict_saturation_capture(self, capture_param_k, trap_density,
-                                   incoming_filled_traps,
-                                   sattime, sat_count, ngroups):
-        """Compute number of traps filled due to saturated pixels.
+    def predict_saturation_capture(
+        self, capture_param_k, trap_density, incoming_filled_traps, sattime, sat_count, ngroups
+    ):
+        """
+        Compute number of traps filled due to saturated pixels.
 
         This is based on Michael Regan's predictsaturationcapture.pro.
 
-        Extended Summary
-        ----------------
         This should not be called for ramps that do not have any groups
         that exceed the persistence saturation limit.  One reason is that
         `incoming_filled_traps` can be so small that `exp_filled_traps`
@@ -936,13 +932,12 @@ def predict_saturation_capture(self, capture_param_k, trap_density,
         ndarray, 2-D
             The computed traps_filled at the end of the integration.
         """
-
         (par0, par1, par2) = capture_param_k
-        par1 = abs(par1)        # the minus sign will be specified explicitly
+        par1 = abs(par1)  # the minus sign will be specified explicitly
 
         # For each pixel that had no ramp before saturation, fill all the
         # instantaneous traps; otherwise, they were filled during the ramp.
-        flag = (sat_count == ngroups)
+        flag = sat_count == ngroups
         incoming_filled_traps[flag] = trap_density[flag] * par2
 
         # Find out how many exponential traps have already been filled.
@@ -955,17 +950,15 @@ def predict_saturation_capture(self, capture_param_k, trap_density,
         # Filling of the empty traps depends only on the exponential
         # component; the instantaneous component would have been filled
         # during the ramp or above.
-        new_filled_traps = empty_traps * (1. - np.exp(-par1 * sattime))
+        new_filled_traps = empty_traps * (1.0 - np.exp(-par1 * sattime))
         total_filled_traps = incoming_filled_traps + new_filled_traps
 
         return total_filled_traps
 
-    def delta_fcn_capture(self, capture_param_k, trap_density, integ,
-                          grp_slope, ngroups, t_group):
-        """Compute number of traps filled due to cosmic-ray jumps.
+    def delta_fcn_capture(self, capture_param_k, trap_density, integ, grp_slope, ngroups, t_group):
+        """
+        Compute number of traps filled due to cosmic-ray jumps.
 
-        Extended Summary
-        ----------------
         If a cosmic-ray hit was in group number g (meaning that
         `data[integ, g, y, x]` was higher than expected), then
         delta_t = (ngroups - g - 0.5) * t_group
@@ -1007,7 +1000,6 @@ def delta_fcn_capture(self, capture_param_k, trap_density, integ,
         ndarray, 2-D
             The computed cr_filled at the end of the integration.
         """
-
         (par0, par1, par2) = capture_param_k
         # cr_filled will be incremented group-by-group, depending on
         # where cosmic rays were found in each group.
@@ -1020,8 +1012,7 @@ def delta_fcn_capture(self, capture_param_k, trap_density, integ,
         # amplitude, so skip the first group.  Loop over all subsequent
         # groups, checking (via the groupdq extension) for cosmic-ray hits.
         for group in range(1, ngroups):
-            cr_flag = np.where(np.bitwise_and(gdq[group, :, :],
-                                              gdqflags['JUMP_DET']) > 0)
+            cr_flag = np.where(np.bitwise_and(gdq[group, :, :], gdqflags["JUMP_DET"]) > 0)
             if len(cr_flag[0]) > 0:
                 delta_t = (float(ngroups - group) - 0.5) * t_group
                 # z and z_prev are index arrays.
@@ -1029,19 +1020,22 @@ def delta_fcn_capture(self, capture_param_k, trap_density, integ,
                 z[:] = group
                 z_prev = z - 1
                 # jump is a 1-D array, just for the CRs in the current group.
-                jump = ((data[z, cr_flag[0], cr_flag[1]]
-                         - data[z_prev, cr_flag[0], cr_flag[1]])
-                        - grp_slope[cr_flag])
-                jump = np.where(jump < 0., 0., jump)
-                cr_filled[cr_flag] += trap_density[cr_flag] * jump \
-                    * (par0 * (1. - math.exp(par1 * delta_t))
-                       + par2)
+                jump = (
+                    data[z, cr_flag[0], cr_flag[1]]
+                    - data[z_prev, cr_flag[0], cr_flag[1]]
+                    - grp_slope[cr_flag]
+                )
+                jump = np.where(jump < 0.0, 0.0, jump)
+                cr_filled[cr_flag] += (
+                    trap_density[cr_flag] * jump * (par0 * (1.0 - math.exp(par1 * delta_t)) + par2)
+                )
 
         cr_filled *= SCALEFACTOR
         return cr_filled
 
     def compute_decay(self, traps_filled, decay_param, delta_t):
-        """Compute the number of trap decays.
+        """
+        Compute the number of trap decays.
 
         This is based on Michael Regan's trapdecay.pro.
 
@@ -1066,11 +1060,10 @@ def compute_decay(self, traps_filled, decay_param, delta_t):
             Image of the computed number of trap decays for each pixel,
             for the current trap family.
         """
-
-        if decay_param == 0.:
-            decayed = traps_filled * 0.
+        if decay_param == 0.0:
+            decayed = traps_filled * 0.0
         else:
-            tau = 1. / abs(decay_param)
-            decayed = traps_filled * (1. - math.exp(-delta_t / tau))
+            tau = 1.0 / abs(decay_param)
+            decayed = traps_filled * (1.0 - math.exp(-delta_t / tau))
 
         return decayed
diff --git a/jwst/persistence/persistence_step.py b/jwst/persistence/persistence_step.py
index 569b2050d76..f8db9d526a9 100644
--- a/jwst/persistence/persistence_step.py
+++ b/jwst/persistence/persistence_step.py
@@ -8,9 +8,7 @@
 
 
 class PersistenceStep(Step):
-    """
-    PersistenceStep: Correct a science image for persistence.
-    """
+    """PersistenceStep: Correct a science image for persistence."""
 
     class_alias = "persistence"
 
@@ -20,23 +18,31 @@ class PersistenceStep(Step):
         save_persistence = boolean(default=False) # Save subtracted persistence to an output file with suffix '_output_pers'
         save_trapsfilled = boolean(default=True) # Save updated trapsfilled file with suffix '_trapsfilled'
         modify_input = boolean(default=False)
-    """ # noqa: E501
+    """  # noqa: E501
 
     reference_file_types = ["trapdensity", "trappars", "persat"]
 
     def process(self, step_input):
-
+        """
+        Execute the persistence correction step.
+
+        Parameters
+        ----------
+        step_input : DataModel or str
+            Input datamodel or file to be corrected
+
+        Returns
+        -------
+        output_model : DataModel
+            The persistence corrected datamodel
+        """
         if self.input_trapsfilled is not None:
-            if (self.input_trapsfilled == "None" or
-                    len(self.input_trapsfilled) == 0):
+            if (self.input_trapsfilled == "None") or (len(self.input_trapsfilled) == 0):
                 self.input_trapsfilled = None
 
         with datamodels.RampModel(step_input) as input_model:
-
-            self.trap_density_filename = self.get_reference_file(input_model,
-                                                                 "trapdensity")
-            self.trappars_filename = self.get_reference_file(input_model,
-                                                             "trappars")
+            self.trap_density_filename = self.get_reference_file(input_model, "trapdensity")
+            self.trappars_filename = self.get_reference_file(input_model, "trappars")
             self.persat_filename = self.get_reference_file(input_model, "persat")
 
             # Is any reference file missing?
@@ -57,7 +63,7 @@ def process(self, step_input):
                 else:
                     msg = "Missing reference file types: "
                     for name in missing_reftypes:
-                        msg += (" " + name)
+                        msg += " " + name
                 self.log.warning("%s", msg)
                 input_model.meta.cal_step.persistence = "SKIPPED"
                 return input_model
@@ -68,35 +74,35 @@ def process(self, step_input):
             if self.input_trapsfilled is None:
                 traps_filled_model = None
             else:
-                traps_filled_model = datamodels.TrapsFilledModel(
-                    self.input_trapsfilled)
-            trap_density_model = datamodels.TrapDensityModel(
-                self.trap_density_filename)
+                traps_filled_model = datamodels.TrapsFilledModel(self.input_trapsfilled)
+            trap_density_model = datamodels.TrapDensityModel(self.trap_density_filename)
             trappars_model = datamodels.TrapParsModel(self.trappars_filename)
             persat_model = datamodels.PersistenceSatModel(self.persat_filename)
 
-            pers_a = persistence.DataSet(result, traps_filled_model,
-                                         self.flag_pers_cutoff,
-                                         self.save_persistence,
-                                         trap_density_model, trappars_model,
-                                         persat_model)
+            pers_a = persistence.DataSet(
+                result,
+                traps_filled_model,
+                self.flag_pers_cutoff,
+                self.save_persistence,
+                trap_density_model,
+                trappars_model,
+                persat_model,
+            )
             (result, traps_filled, output_pers, skipped) = pers_a.do_all()
             if skipped:
-                result.meta.cal_step.persistence = 'SKIPPED'
+                result.meta.cal_step.persistence = "SKIPPED"
             else:
-                result.meta.cal_step.persistence = 'COMPLETE'
+                result.meta.cal_step.persistence = "COMPLETE"
 
-            if traps_filled_model is not None:      # input traps_filled
+            if traps_filled_model is not None:  # input traps_filled
                 del traps_filled_model
-            if traps_filled is not None:            # output traps_filled
+            if traps_filled is not None:  # output traps_filled
                 # Save the traps_filled image with suffix 'trapsfilled'.
-                self.save_model(
-                    traps_filled, suffix='trapsfilled', force=self.save_trapsfilled
-                )
+                self.save_model(traps_filled, "trapsfilled", force=self.save_trapsfilled)
                 del traps_filled
 
-            if output_pers is not None:             # output file of persistence
-                self.save_model(output_pers, suffix='output_pers')
+            if output_pers is not None:  # output file of persistence
+                self.save_model(output_pers, suffix="output_pers")
                 output_pers.close()
 
             # Cleanup
diff --git a/jwst/persistence/tests/__init__.py b/jwst/persistence/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/jwst/persistence/tests/test_persistence.py b/jwst/persistence/tests/test_persistence.py
index a2a941a1e69..e8408d6a653 100644
--- a/jwst/persistence/tests/test_persistence.py
+++ b/jwst/persistence/tests/test_persistence.py
@@ -9,22 +9,23 @@
 from jwst.persistence import persistence
 
 
-def test_no_NaN():
+def test_no_nan():
+    """Test the no_nan function."""
     input_model = datamodels.RampModel()
     input_model.data = 2.0 * np.ones((5, 10, 10, 10))
     # Datamodel with no nans or zeros should be unchanged
-    result = persistence.no_NaN(input_model, 1.0, zap_nan=True, zap_zero=True)
+    result = persistence.no_nan(input_model, 1.0, zap_nan=True, zap_zero=True)
     np.testing.assert_equal(result.data, input_model.data)
 
     input_model.data[0] = np.nan
     input_model.data[1] = 0.0
     # Test replacing both NaNs and zeros
-    result = persistence.no_NaN(input_model, 12.0, zap_nan=True, zap_zero=True)
+    result = persistence.no_nan(input_model, 12.0, zap_nan=True, zap_zero=True)
     np.testing.assert_equal(result.data[0], 12.0)
     np.testing.assert_equal(result.data[1], 12.0)
     np.testing.assert_equal(result.data[2:], 2.0)
     # Test replacing only NaNs
-    result = persistence.no_NaN(input_model, 11.0, zap_nan=True)
+    result = persistence.no_nan(input_model, 11.0, zap_nan=True)
     np.testing.assert_equal(result.data[0], 11.0)
     np.testing.assert_equal(result.data[1], 0.0)
     np.testing.assert_equal(result.data[2:], 2.0)
@@ -68,7 +69,7 @@ def create_traps_filled_model():
     Returns
     -------
     trapsfilled_model : TrapsFilledModel DataModel
-        Sample TrapsFilledModel datamodel for testing
+        Sample TrapsFilledModel for testing
     """
 
     def _dm(nrows, ncols):
@@ -90,7 +91,7 @@ def create_trap_density_model():
     Returns
     -------
     trapdensity_model : TrapDensityModel DataModel
-        Sample TrapDensityModel datamodel for testing
+        Sample TrapDensityModel for testing
     """
 
     def _dm(nrows, ncols):
@@ -110,7 +111,7 @@ def create_trappars_model():
     Returns
     -------
     trappars_model : TrapParsModel DataModel
-        Sample TrapParsModel datamodel for testing
+        Sample TrapParsModel for testing
     """
     capture0 = fits.Column(name="capture0", array=np.array([180.0, 270.0, 80.0]), format="D")
     capture1 = fits.Column(name="capture1", array=np.array([-0.0004, -0.004, -0.0009]), format="D")
@@ -134,6 +135,14 @@ def test_get_slice(create_sci_model, create_traps_filled_model):
 
 
 def test_ref_matches_sci(create_traps_filled_model):
+    """
+    Test the ref_matches_sci method.
+
+    Parameters
+    ----------
+    create_traps_filled_model : pytest fixture
+        Fixture that returns a TrapsFilledModel for testing
+    """
     # Create empty DataSet
     ds = persistence.DataSet(None, None, 40.0, False, None, None, None)
     tf = create_traps_filled_model(2048, 2048)
@@ -142,7 +151,7 @@ def test_ref_matches_sci(create_traps_filled_model):
         (slice(256, 768, None), slice(1024, 1536, None)),
     ]
     expected_results = [True, False]
-    for this_slice, expected in zip(test_slices, expected_results):
+    for this_slice, expected in zip(test_slices, expected_results, strict=True):
         assert ds.ref_matches_sci(tf, this_slice) == expected
 
 
@@ -163,6 +172,14 @@ def test_get_subarray(create_trap_density_model):
 
 @pytest.fixture(scope="class")
 def generate_trap_pars():
+    """
+    Fixture to create arrays of trappar parameters.
+
+    Returns
+    -------
+    pars : tuple of 4 numpy.ndarrays
+        The tuple of parameter arrays
+    """
     par0 = np.array([180.0, 270.0, 80.0])
     par1 = np.array([-0.0004, -0.004, -0.0009])
     par2 = np.array([290.0, 140.0, 320.0])
@@ -175,6 +192,7 @@ class TrapParsTester:
     """Class to handle testing trap pars."""
 
     def test_get_parameters(self):
+        """Test the get_parameters method."""
         # Create empty DataSet
         ds = persistence.DataSet(None, None, 40.0, False, None, None, None)
         ds.trappars_model = create_trappars_model()
@@ -188,8 +206,10 @@ def test_get_capture_param(self, generate_trap_pars):
         """
         Test the get_capture_param method.
 
-        Input data is a tuple of 4 float arrays of length 3
-        But still need to create an empty dataset
+        Parameters
+        ----------
+        generate_trap_pars : pytest fixture
+            Fixture that returns trappars parameter arrays for testing
         """
         ds = persistence.DataSet(None, None, 40.0, False, None, None, None)
         pars = generate_trap_pars()
@@ -200,6 +220,14 @@ def test_get_capture_param(self, generate_trap_pars):
         assert np.allclose(returned_pars[2], 140.0, rtol=1.0e-7, atol=1.0e-7)
 
     def test_get_decay_param(self, generate_trap_pars):
+        """
+        Test the get_decay_param method.
+
+        Parameters
+        ----------
+        generate_trap_pars : pytest fixture
+            Fixture that returns TrapPars parameter arrays for testing
+        """
         ds = persistence.DataSet(None, None, 40.0, False, None, None, None)
         pars = generate_trap_pars()
         index = 1
@@ -208,6 +236,14 @@ def test_get_decay_param(self, generate_trap_pars):
 
 
 def test_get_group_info(create_sci_model):
+    """
+    Test the get_group_info method.
+
+    Parameters
+    ----------
+    create_sci_model : pytest fixture
+        Fixture that returns a science DataModel for testing
+    """
     output_model = create_sci_model(3, 12, 512, 512, 257, 769)
     ds = persistence.DataSet(output_model, None, 40.0, False, None, None, None)
     integration = 1
@@ -221,6 +257,14 @@ def test_get_group_info(create_sci_model):
 
 
 def create_persistencesat_model():
+    """
+    Create a PersistencesatModel for testing.
+
+    Returns
+    -------
+    persat_model : DataModel
+        The minimal PersistencesatModel for testing
+    """
     scidata = np.ones((2048, 2048), dtype=np.float32)
     scidata = scidata[:] * 40.0
     persat_model = datamodels.PersistenceSatModel(data=scidata)
@@ -233,17 +277,14 @@ def test_do_all(create_sci_model, create_traps_filled_model, create_trap_density
     """
     Test the do_all method.
 
-    Needs a Dataset object with:
-        .output_obj.data.shape (which must have len 4, like a RampModel)
-        .output_obj.meta.exposure.group_time
-        .trap_density.data.shape
-        .traps_filled (will create one if None)
-        .trap_density
-        .persistencesat
-        .output_obj.meta.exposure.start_time
-        .output_obj.meta.exposure.end_time
-        .traps_filled.data
-        .save_persistence
+    Parameters
+    ----------
+    create_sci_model : pytest fixture
+        Fixture that returns a science DataModel for testing
+    create_traps_filled_model : pytest fixture
+        Fixture that returns a TrapsFilledModel for testing
+    create_trap_density_model : pytest fixture
+        Fixture that returns a TrapDensityModel for testing
     """
     input_model = create_sci_model(3, 12, 512, 512, 257, 769)
     traps_filled_model = create_traps_filled_model(2048, 2048)


Reviews:
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:11:19Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:14:29Z
user: login: pllim
body: Drive-by review. Thanks!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:16:22Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:17:07Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:20:18Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:23:10Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:23:57Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T16:14:41Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T16:15:20Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T16:30:42Z
user: login: melanieclarke
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T16:53:27Z
user: login: kmacdonald-stsci
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:25:35Z
user: login: melanieclarke
body: Mostly looking good, thanks!  A few style comments below.

I'd still like to add some minimal code exercise tests if we can to make sure we're not breaking things here.  Have you run the code locally before and after your changes?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T14:09:14Z
user: login: emolter
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:28:40Z
user: login: emolter
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T14:31:19Z
user: login: pllim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T16:03:49Z
user: login: emolter
body: Looks great, just a few very small comments
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T14:25:56Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:29:42Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:54:24Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:55:05Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:55:24Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:57:24Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:00:45Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:01:16Z
user: login: stscirij
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:01:35Z
user: login: melanieclarke
body: Tests are passing.  Thanks for getting this cleaned up!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T20:11:14Z

Review comments:
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: why skip?
created_at: 2025-04-10T15:11:19Z
author_association: COLLABORATOR
diff_hunk: @@ -115,16 +120,23 @@ class DataSet():
         The number of resets (frames) at the beginning of each integration.
     """
 
-    def __init__(self, output_obj, input_traps_filled,
-                 flag_pers_cutoff, save_persistence,
-                 trap_density_model, trappars_model,
-                 persat_model):
-        """Assign values to attributes.
+    def __init__(
+        self,
+        output_obj,
+        input_traps_filled,
+        flag_pers_cutoff,
+        save_persistence,
+        trap_density_model,
+        trappars_model,
+        persat_model,
+    ):
+        """
+        Assign values to attributes.
 
         Parameters
         ----------
         output_obj : JWST data model
-            copy of input data model object
+            Copy of input data model object
path: jwst/persistence/persistence.py
user: login: pllim
body: ```suggestion
            Copy of input data model object.
```
created_at: 2025-04-10T15:11:46Z
author_association: COLLABORATOR
diff_hunk: @@ -221,7 +232,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: Why skip instead of removing redundant parenthesis? (Same question as above and same question for all similar occurrences.)
created_at: 2025-04-10T15:12:21Z
author_association: COLLABORATOR
diff_hunk: @@ -230,21 +241,20 @@ def do_all(self):
         if not self.traps_filled:
             have_traps_filled = False
             self.traps_filled = datamodels.TrapsFilledModel(
-                data=np.zeros((nfamilies, det_ny, det_nx),
-                              dtype=self.output_obj.data.dtype)
+                data=np.zeros((nfamilies, det_ny, det_nx), dtype=self.output_obj.data.dtype)
             )
             self.traps_filled.meta.subarray.xstart = 1
             self.traps_filled.meta.subarray.ystart = 1
             self.traps_filled.meta.subarray.xsize = det_nx
             self.traps_filled.meta.subarray.ysize = det_ny
         else:
             have_traps_filled = True
-            self.traps_filled = no_NaN(self.traps_filled, 0., zap_nan=True)
+            self.traps_filled = no_NaN(self.traps_filled, 0.0, zap_nan=True)
 
-        self.trap_density = no_NaN(self.trap_density, 0., zap_nan=True)
-        self.persistencesat = no_NaN(self.persistencesat, 1.e7, zap_nan=True)
+        self.trap_density = no_NaN(self.trap_density, 0.0, zap_nan=True)
+        self.persistencesat = no_NaN(self.persistencesat, 1.0e7, zap_nan=True)
path: jwst/persistence/persistence.py
user: login: pllim
body: Same suggestion for all similar occurrences.

```suggestion
        self.persistencesat = no_NaN(self.persistencesat, 1e7, zap_nan=True)
```
created_at: 2025-04-10T15:12:46Z
author_association: COLLABORATOR
diff_hunk: @@ -567,14 +571,14 @@ def compute_slope(self, integ):
             The ramp slope in units of fraction of the persistence
             saturation limit per second.
         """
-
         (_, ngroups, ny, nx) = self.output_obj.shape
         if ngroups == 1:
             # This won't be accurate, because there's only one group.
             grp_slope = self.output_obj.data[integ, 0, :, :]
             if hasattr(self.persistencesat, "dq"):
-                mask = (np.bitwise_and(self.persistencesat.dq,
-                                       dqflags.pixel["DO_NOT_USE"]) > 0)
+                mask = (
+                    np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+                )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: What are we skipping here? (Same question for all similar occurrences.)
created_at: 2025-04-10T15:14:03Z
author_association: COLLABORATOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: For clarity  - removing the parentheses relies on the reader being intimately familiar with operator precedence rules while keeping them in makes it obvious
created_at: 2025-04-10T15:14:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: Is it possible to tell the skip directive to only skip that one rule you are skipping? This looks like a "skip all checks"?
created_at: 2025-04-10T15:17:07Z
author_association: COLLABORATOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: It should only skip the line
created_at: 2025-04-10T15:20:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: Right. But we want to know of other violations besides the parenthesis for that line, right?
created_at: 2025-04-10T15:23:10Z
author_association: COLLABORATOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: pllim
body: Would a `# noqa: <rule code>` work here?
created_at: 2025-04-10T15:23:57Z
author_association: COLLABORATOR
diff_hunk: @@ -230,21 +241,20 @@ def do_all(self):
         if not self.traps_filled:
             have_traps_filled = False
             self.traps_filled = datamodels.TrapsFilledModel(
-                data=np.zeros((nfamilies, det_ny, det_nx),
-                              dtype=self.output_obj.data.dtype)
+                data=np.zeros((nfamilies, det_ny, det_nx), dtype=self.output_obj.data.dtype)
             )
             self.traps_filled.meta.subarray.xstart = 1
             self.traps_filled.meta.subarray.ystart = 1
             self.traps_filled.meta.subarray.xsize = det_nx
             self.traps_filled.meta.subarray.ysize = det_ny
         else:
             have_traps_filled = True
-            self.traps_filled = no_NaN(self.traps_filled, 0., zap_nan=True)
+            self.traps_filled = no_NaN(self.traps_filled, 0.0, zap_nan=True)
 
-        self.trap_density = no_NaN(self.trap_density, 0., zap_nan=True)
-        self.persistencesat = no_NaN(self.persistencesat, 1.e7, zap_nan=True)
+        self.trap_density = no_NaN(self.trap_density, 0.0, zap_nan=True)
+        self.persistencesat = no_NaN(self.persistencesat, 1.0e7, zap_nan=True)
path: jwst/persistence/persistence.py
user: login: stscirij
body: ruff format insists on the .0
created_at: 2025-04-10T16:14:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,16 +120,23 @@ class DataSet():
         The number of resets (frames) at the beginning of each integration.
     """
 
-    def __init__(self, output_obj, input_traps_filled,
-                 flag_pers_cutoff, save_persistence,
-                 trap_density_model, trappars_model,
-                 persat_model):
-        """Assign values to attributes.
+    def __init__(
+        self,
+        output_obj,
+        input_traps_filled,
+        flag_pers_cutoff,
+        save_persistence,
+        trap_density_model,
+        trappars_model,
+        persat_model,
+    ):
+        """
+        Assign values to attributes.
 
         Parameters
         ----------
         output_obj : JWST data model
-            copy of input data model object
+            Copy of input data model object
path: jwst/persistence/persistence.py
user: login: stscirij
body: Only actual sentences need the period.
created_at: 2025-04-10T16:15:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -255,14 +265,13 @@ def do_all(self):
             # Decays during the time covered by the reset (again, if any)
             # between integrations will be taken care of in the loop
             # over integrations.
-            to_start = (self.output_obj.meta.exposure.start_time
-                        - self.traps_filled.meta.exposure.end_time) * 86400.
-            log.debug("Decay time for previous traps-filled file = %g s",
-                      to_start)
+            to_start = (
+                self.output_obj.meta.exposure.start_time - self.traps_filled.meta.exposure.end_time
+            ) * 86400.0
path: jwst/persistence/persistence.py
user: login: kmacdonald-stsci
body: For readability, I recommend a two step process like below.

```
            to_start = self.output_obj.meta.exposure.start_time - self.traps_filled.meta.exposure.end_time
            to_start *= 86400.0
```
created_at: 2025-04-10T16:21:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -605,12 +608,14 @@ def compute_slope(self, integ):
         diff = data[1:, :, :] - data[0:-1, :, :]
         max_diff = diff.max()
         # Larger than any actual data value
-        huge_diff = max(2. * max_diff, 1.e5)
+        huge_diff = max(2.0 * max_diff, 1.0e5)
         del max_diff
 
         # This assumes that the saturation step has already been run.
         # n_sat is a 2-D array of the number of saturated groups per pixel.
-        s_mask = (np.bitwise_and(gdq[1:, :, :], gdqflags["SATURATED"]) > 0)
+        s_mask = (
path: jwst/persistence/persistence.py
user: login: kmacdonald-stsci
body: Does this really need to be split across multiple lines?
created_at: 2025-04-10T16:24:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -870,32 +873,34 @@ def predict_ramp_capture(self, capture_param_k, trap_density, slope, dt):
         ndarray, 2-D
             The computed traps_filled at the end of the integration.
         """
-
         (par0, par1, par2) = capture_param_k
         if par1 == 0:
-            log.error("Capture parameter is zero; parameters are %g, %g, %g",
-                      par0, par1, par2)
-            tau = 1.e10                 # arbitrary "big" number
+            log.error("Capture parameter is zero; parameters are %g, %g, %g", par0, par1, par2)
+            tau = 1.0e10  # arbitrary "big" number
         else:
-            tau = 1. / abs(par1)
-
-        traps_filled = (trap_density * slope**2
-                        * (dt**2 * (par0 + par2) / 2.
-                           + par0 * (dt * tau + tau**2) * np.exp(-dt / tau)
-                           - par0 * tau**2))
+            tau = 1.0 / abs(par1)
+
+        traps_filled = (
path: jwst/persistence/persistence.py
user: login: kmacdonald-stsci
body: I find multiline math computations difficult to read.  I recommend intermediate values, like this:

```
        traps_filled = trap_density * slope**2
        tmp = dt**2 * (par0 + par2) / 2.0
        tmp += par0 * (dt * tau + tau**2) * np.exp(-dt / tau)
        tmp -= par0 * tau**2
        traps_filled *= tmp
```
created_at: 2025-04-10T16:27:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1020,28 +1021,30 @@ def delta_fcn_capture(self, capture_param_k, trap_density, integ,
         # amplitude, so skip the first group.  Loop over all subsequent
         # groups, checking (via the groupdq extension) for cosmic-ray hits.
         for group in range(1, ngroups):
-            cr_flag = np.where(np.bitwise_and(gdq[group, :, :],
-                                              gdqflags['JUMP_DET']) > 0)
+            cr_flag = np.where(np.bitwise_and(gdq[group, :, :], gdqflags["JUMP_DET"]) > 0)
             if len(cr_flag[0]) > 0:
                 delta_t = (float(ngroups - group) - 0.5) * t_group
                 # z and z_prev are index arrays.
                 z = np.empty(len(cr_flag[0]), dtype=np.intp)
                 z[:] = group
                 z_prev = z - 1
                 # jump is a 1-D array, just for the CRs in the current group.
-                jump = ((data[z, cr_flag[0], cr_flag[1]]
-                         - data[z_prev, cr_flag[0], cr_flag[1]])
-                        - grp_slope[cr_flag])
-                jump = np.where(jump < 0., 0., jump)
-                cr_filled[cr_flag] += trap_density[cr_flag] * jump \
-                    * (par0 * (1. - math.exp(par1 * delta_t))
-                       + par2)
+                jump = (
path: jwst/persistence/persistence.py
user: login: kmacdonald-stsci
body: As above, I find multiline math operations difficult to read and recommend rewriting this as a series of intermediate steps.
created_at: 2025-04-10T16:30:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,21 +241,20 @@ def do_all(self):
         if not self.traps_filled:
             have_traps_filled = False
             self.traps_filled = datamodels.TrapsFilledModel(
-                data=np.zeros((nfamilies, det_ny, det_nx),
-                              dtype=self.output_obj.data.dtype)
+                data=np.zeros((nfamilies, det_ny, det_nx), dtype=self.output_obj.data.dtype)
             )
             self.traps_filled.meta.subarray.xstart = 1
             self.traps_filled.meta.subarray.ystart = 1
             self.traps_filled.meta.subarray.xsize = det_nx
             self.traps_filled.meta.subarray.ysize = det_ny
         else:
             have_traps_filled = True
-            self.traps_filled = no_NaN(self.traps_filled, 0., zap_nan=True)
+            self.traps_filled = no_NaN(self.traps_filled, 0.0, zap_nan=True)
 
-        self.trap_density = no_NaN(self.trap_density, 0., zap_nan=True)
-        self.persistencesat = no_NaN(self.persistencesat, 1.e7, zap_nan=True)
+        self.trap_density = no_NaN(self.trap_density, 0.0, zap_nan=True)
+        self.persistencesat = no_NaN(self.persistencesat, 1.0e7, zap_nan=True)
path: jwst/persistence/persistence.py
user: login: pllim
body: Huh! I thought maybe it was the period that triggered it and removing the period would also work but I am not sure.
created_at: 2025-04-10T16:30:42Z
author_association: COLLABORATOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: Okay, I looked into this, and it looks like Robert is right - the only way to get the formatter to skip this kind of update is to turn it off for this line of code.  There is no way to tell it to leave non-semantic parentheses alone -- unlike the linter, the formatter has very minimal configuration options, following Black's example as a non-compromising formatter.

Since we have agreed to use the ruff formatter for our code, I would prefer to allow it to work on as many lines as possible.  I am okay with occasionally skipping formatting for lines if you feel it's really harming comprehension, but I'll ask you to use your best judgement on when that is, and keep the skips to a minimum.
created_at: 2025-04-10T16:53:27Z
author_association: COLLABORATOR
diff_hunk: @@ -11,15 +11,20 @@
 log = logging.getLogger(__name__)
 log.setLevel(logging.DEBUG)
 
-SCALEFACTOR = 2.
+SCALEFACTOR = 2.0
 """This factor is to account for the difference in gain for charges freed
 from traps, compared with photon-generated charges.
 """
 
 
-def no_NaN(input_model, fill_value,
-           zap_nan=False, zap_zero=False):
-    """Replace NaNs and/or zeros with a fill value.
+def no_NaN(  # noqa: N802
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: Why not fix the function naming here?  It looks like its use is contained only in this file, so it should have no impact to rename it properly.
created_at: 2025-04-14T13:40:18Z
author_association: COLLABORATOR
diff_hunk: @@ -221,7 +232,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: How about this, to make it more readable but also keep the formatter happy?
```suggestion
        is_subarray = (shape[-2] != det_ny) or (shape[-1] != det_nx)
```
created_at: 2025-04-14T13:44:30Z
author_association: COLLABORATOR
diff_hunk: @@ -230,21 +241,20 @@ def do_all(self):
         if not self.traps_filled:
             have_traps_filled = False
             self.traps_filled = datamodels.TrapsFilledModel(
-                data=np.zeros((nfamilies, det_ny, det_nx),
-                              dtype=self.output_obj.data.dtype)
+                data=np.zeros((nfamilies, det_ny, det_nx), dtype=self.output_obj.data.dtype)
             )
             self.traps_filled.meta.subarray.xstart = 1
             self.traps_filled.meta.subarray.ystart = 1
             self.traps_filled.meta.subarray.xsize = det_nx
             self.traps_filled.meta.subarray.ysize = det_ny
         else:
             have_traps_filled = True
-            self.traps_filled = no_NaN(self.traps_filled, 0., zap_nan=True)
+            self.traps_filled = no_NaN(self.traps_filled, 0.0, zap_nan=True)
 
-        self.trap_density = no_NaN(self.trap_density, 0., zap_nan=True)
-        self.persistencesat = no_NaN(self.persistencesat, 1.e7, zap_nan=True)
+        self.trap_density = no_NaN(self.trap_density, 0.0, zap_nan=True)
+        self.persistencesat = no_NaN(self.persistencesat, 1.0e7, zap_nan=True)
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: I think Pey Lian is right - you can either do `1e7` or `1.0e7` but not `1.e7`.
created_at: 2025-04-14T13:45:57Z
author_association: COLLABORATOR
diff_hunk: @@ -567,14 +571,14 @@ def compute_slope(self, integ):
             The ramp slope in units of fraction of the persistence
             saturation limit per second.
         """
-
         (_, ngroups, ny, nx) = self.output_obj.shape
         if ngroups == 1:
             # This won't be accurate, because there's only one group.
             grp_slope = self.output_obj.data[integ, 0, :, :]
             if hasattr(self.persistencesat, "dq"):
-                mask = (np.bitwise_and(self.persistencesat.dq,
-                                       dqflags.pixel["DO_NOT_USE"]) > 0)
+                mask = (
+                    np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+                )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: I don't think skipping formatting is improving readability here.
created_at: 2025-04-14T13:54:09Z
author_association: COLLABORATOR
diff_hunk: @@ -605,12 +608,14 @@ def compute_slope(self, integ):
         diff = data[1:, :, :] - data[0:-1, :, :]
         max_diff = diff.max()
         # Larger than any actual data value
-        huge_diff = max(2. * max_diff, 1.e5)
+        huge_diff = max(2.0 * max_diff, 1.0e5)
         del max_diff
 
         # This assumes that the saturation step has already been run.
         # n_sat is a 2-D array of the number of saturated groups per pixel.
-        s_mask = (np.bitwise_and(gdq[1:, :, :], gdqflags["SATURATED"]) > 0)
+        s_mask = (
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: Here also - skipping formatting leaves this line worse off.
created_at: 2025-04-14T13:54:25Z
author_association: COLLABORATOR
diff_hunk: @@ -620,27 +625,28 @@ def compute_slope(self, integ):
         # Upper limit of good data (slice notation, i.e. exclusive).
         upper = diff.shape[0] - (n_cr + n_sat)
         really_bad = np.where(upper < 0)
-        upper[really_bad] = 0           # for the comparison with indx below
+        upper[really_bad] = 0  # for the comparison with indx below
         del really_bad
         temp = np.arange(ngroups - 1, dtype=np.int32)
         temp2 = np.repeat(temp, nx * ny)
         indx = temp2.reshape((ngroups - 1, ny, nx))
         mask = np.where(indx >= upper)
-        sdiff[mask] = 0.                # zero values won't affect the sum
+        sdiff[mask] = 0.0  # zero values won't affect the sum
         del temp, temp2, indx, mask
         bad = np.where(upper <= 0)
-        upper[bad] = 1                  # so we can divide by upper
+        upper[bad] = 1  # so we can divide by upper
 
         # grp_slope has units of DN / group
         grp_slope = np.sum(sdiff, axis=0) / upper.astype(np.float32)
-        grp_slope[bad] = 0.
+        grp_slope[bad] = 0.0
         del bad
 
         # slope will have units (DN / persistence_saturation_limit) / second,
         # where persistence_saturation_limit is in units of DN.
         if hasattr(self.persistencesat, "dq"):
-            mask = (np.bitwise_and(self.persistencesat.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
+            mask = (
+                np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+            )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: Here too
```suggestion
            mask = np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
```
created_at: 2025-04-14T13:54:45Z
author_association: COLLABORATOR
diff_hunk: @@ -796,10 +797,11 @@ def predict_capture(self, capture_param_k, trap_density, integ,
         totaltime = ngroups * t_group + nresets * t_frame
 
         # Find pixels exceeding the persistence saturation limit (full well).
-        pflag = (data > self.persistencesat.data)
+        pflag = (data > self.persistencesat.data)  # fmt: skip
         if hasattr(self.persistencesat, "dq"):
-            mask = (np.bitwise_and(self.persistencesat.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
+            mask = (
+                np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+            )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: ```suggestion
            mask = np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
```
created_at: 2025-04-14T13:55:49Z
author_association: COLLABORATOR
diff_hunk: @@ -870,32 +873,34 @@ def predict_ramp_capture(self, capture_param_k, trap_density, slope, dt):
         ndarray, 2-D
             The computed traps_filled at the end of the integration.
         """
-
         (par0, par1, par2) = capture_param_k
         if par1 == 0:
-            log.error("Capture parameter is zero; parameters are %g, %g, %g",
-                      par0, par1, par2)
-            tau = 1.e10                 # arbitrary "big" number
+            log.error("Capture parameter is zero; parameters are %g, %g, %g", par0, par1, par2)
+            tau = 1.0e10  # arbitrary "big" number
         else:
-            tau = 1. / abs(par1)
-
-        traps_filled = (trap_density * slope**2
-                        * (dt**2 * (par0 + par2) / 2.
-                           + par0 * (dt * tau + tau**2) * np.exp(-dt / tau)
-                           - par0 * tau**2))
+            tau = 1.0 / abs(par1)
+
+        traps_filled = (
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: I think this is okay as is -  reading the formatted multiline calculation is not much different from reading your suggested change.  Introducing changes to the computations is a little riskier than the other style changes here, since this code is not covered by tests.
created_at: 2025-04-14T13:59:21Z
author_association: COLLABORATOR
diff_hunk: @@ -302,9 +310,10 @@ def do_all(self):
         # file are flagged as bad, set the corresponding data values to
         # zero, so the computed number of traps will also be zero.
         if hasattr(self.trap_density, "dq"):
-            mask = (np.bitwise_and(self.trap_density.dq,
-                                   dqflags.pixel["DO_NOT_USE"]) > 0)
-            self.trap_density.data[mask] = 0.
+            mask = (
+                np.bitwise_and(self.trap_density.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+            )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: As in other similar cases - skipping formatting is not helping readability here.
```suggestion
            mask = np.bitwise_and(self.trap_density.dq, dqflags.pixel["DO_NOT_USE"]) > 0
```
created_at: 2025-04-14T14:02:50Z
author_association: COLLABORATOR
diff_hunk: @@ -1020,28 +1021,30 @@ def delta_fcn_capture(self, capture_param_k, trap_density, integ,
         # amplitude, so skip the first group.  Loop over all subsequent
         # groups, checking (via the groupdq extension) for cosmic-ray hits.
         for group in range(1, ngroups):
-            cr_flag = np.where(np.bitwise_and(gdq[group, :, :],
-                                              gdqflags['JUMP_DET']) > 0)
+            cr_flag = np.where(np.bitwise_and(gdq[group, :, :], gdqflags["JUMP_DET"]) > 0)
             if len(cr_flag[0]) > 0:
                 delta_t = (float(ngroups - group) - 0.5) * t_group
                 # z and z_prev are index arrays.
                 z = np.empty(len(cr_flag[0]), dtype=np.intp)
                 z[:] = group
                 z_prev = z - 1
                 # jump is a 1-D array, just for the CRs in the current group.
-                jump = ((data[z, cr_flag[0], cr_flag[1]]
-                         - data[z_prev, cr_flag[0], cr_flag[1]])
-                        - grp_slope[cr_flag])
-                jump = np.where(jump < 0., 0., jump)
-                cr_filled[cr_flag] += trap_density[cr_flag] * jump \
-                    * (par0 * (1. - math.exp(par1 * delta_t))
-                       + par2)
+                jump = (
path: jwst/persistence/persistence.py
user: login: melanieclarke
body: I would also leave this as is, until we can add more tests for this function.
created_at: 2025-04-14T14:04:53Z
author_association: COLLABORATOR
diff_hunk: @@ -115,16 +120,23 @@ class DataSet():
         The number of resets (frames) at the beginning of each integration.
     """
 
-    def __init__(self, output_obj, input_traps_filled,
-                 flag_pers_cutoff, save_persistence,
-                 trap_density_model, trappars_model,
-                 persat_model):
-        """Assign values to attributes.
+    def __init__(
+        self,
+        output_obj,
+        input_traps_filled,
+        flag_pers_cutoff,
+        save_persistence,
+        trap_density_model,
+        trappars_model,
+        persat_model,
+    ):
+        """
+        Assign values to attributes.
 
         Parameters
         ----------
         output_obj : JWST data model
-            copy of input data model object
+            Copy of input data model object
path: jwst/persistence/persistence.py
user: login: emolter
body: FWIW @pllim this was an executive decision made by me when implementing the style rules in the first place.  I didn't like having periods when the parameter description was a fragment - it annoyed me more than not having periods when the parameter description was a complete sentence.  Of course this is a personal preference, like many style rules are, but I suggest we keep it as-is unless you absolutely hate the current rule.  Otherwise we'll have to submit a pretty silly-looking follow-up PR changing all instances of this across the repo...
created_at: 2025-04-17T14:28:40Z
author_association: COLLABORATOR
diff_hunk: @@ -221,7 +232,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: emolter
body: FWIW I agree with @pllim in these cases - I don't feel it requires "intimate" knowledge to understand the precedence rules for the assignment operators and boolean operators.  For this particular case though, I could see the argument that
```suggestion
        is_subarray = (shape[-2] != det_ny) or (shape[-1] != det_nx)
```
would be the most human-readable
created_at: 2025-04-17T14:31:19Z
author_association: COLLABORATOR
diff_hunk: @@ -115,16 +120,23 @@ class DataSet():
         The number of resets (frames) at the beginning of each integration.
     """
 
-    def __init__(self, output_obj, input_traps_filled,
-                 flag_pers_cutoff, save_persistence,
-                 trap_density_model, trappars_model,
-                 persat_model):
-        """Assign values to attributes.
+    def __init__(
+        self,
+        output_obj,
+        input_traps_filled,
+        flag_pers_cutoff,
+        save_persistence,
+        trap_density_model,
+        trappars_model,
+        persat_model,
+    ):
+        """
+        Assign values to attributes.
 
         Parameters
         ----------
         output_obj : JWST data model
-            copy of input data model object
+            Copy of input data model object
path: jwst/persistence/persistence.py
user: login: pllim
body: Yes, this was explained verbally to me in tag-up, after I posted this comment. It still bothers me because this leads into cases where one parameter description has no period. But then the one right after it has periods because the description had multiple sentences. But I understand it is a lot of churn with little benefit to fix, so okay to ignore.
created_at: 2025-04-18T16:03:48Z
author_association: COLLABORATOR
diff_hunk: @@ -8,9 +8,7 @@
 
 
 class PersistenceStep(Step):
-    """
-    PersistenceStep: Correct a science image for persistence.
-    """
+    """PersistenceStep: Correct a science image for persistence."""
path: jwst/persistence/persistence_step.py
user: login: emolter
body: ```suggestion
    """Correct a science image for persistence."""
```
created_at: 2025-05-07T14:24:33Z
author_association: COLLABORATOR
diff_hunk: @@ -20,23 +18,31 @@ class PersistenceStep(Step):
         save_persistence = boolean(default=False) # Save subtracted persistence to an output file with suffix '_output_pers'
         save_trapsfilled = boolean(default=True) # Save updated trapsfilled file with suffix '_trapsfilled'
         modify_input = boolean(default=False)
-    """ # noqa: E501
+    """  # noqa: E501
 
     reference_file_types = ["trapdensity", "trappars", "persat"]
 
     def process(self, step_input):
-
+        """
+        Execute the persistence correction step.
+
+        Parameters
+        ----------
+        step_input : DataModel
path: jwst/persistence/persistence_step.py
user: login: emolter
body: Can the step be run standalone, and if so, can this also be `str`?
created_at: 2025-05-07T14:25:01Z
author_association: COLLABORATOR
diff_hunk: @@ -134,6 +135,14 @@ def test_get_slice(create_sci_model, create_traps_filled_model):
 
 
 def test_ref_matches_sci(create_traps_filled_model):
+    """
+    Test the ref_matches_sci method.
+
+    Parameters
+    ----------
+    create_traps_filled_model : pytest fixture
+        Fixture that returns a TrapsFilledModel DataModel for testing
path: jwst/persistence/tests/test_persistence.py
user: login: emolter
body: ```suggestion
        Fixture that returns a TrapsFilledModel for testing
```
created_at: 2025-05-07T14:25:27Z
author_association: COLLABORATOR
diff_hunk: @@ -20,23 +18,31 @@ class PersistenceStep(Step):
         save_persistence = boolean(default=False) # Save subtracted persistence to an output file with suffix '_output_pers'
         save_trapsfilled = boolean(default=True) # Save updated trapsfilled file with suffix '_trapsfilled'
         modify_input = boolean(default=False)
-    """ # noqa: E501
+    """  # noqa: E501
 
     reference_file_types = ["trapdensity", "trappars", "persat"]
 
     def process(self, step_input):
-
+        """
+        Execute the persistence correction step.
+
+        Parameters
+        ----------
+        step_input : DataModel
path: jwst/persistence/persistence_step.py
user: login: stscirij
body: Yes, can also be a str
created_at: 2025-05-07T15:29:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,17 +48,16 @@ def no_NaN(input_model, fill_value,
         A copy of `input_model` with NaNs and/or zeros in the `data`
         attribute replaced with `fill_value`.
     """
-
     mask = None
 
     if zap_nan:
-        mask = (np.isnan(input_model.data))
+        mask = np.isnan(input_model.data)
 
     if zap_zero:
         if mask is None:
-            mask = (input_model.data == 0.)
+            mask = (input_model.data == 0.0)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: I took out the parens and skip directives 
created_at: 2025-05-07T15:54:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,7 +232,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: Suggestion taken, I prefer this and ruff is happy too
created_at: 2025-05-07T15:55:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -567,14 +571,14 @@ def compute_slope(self, integ):
             The ramp slope in units of fraction of the persistence
             saturation limit per second.
         """
-
         (_, ngroups, ny, nx) = self.output_obj.shape
         if ngroups == 1:
             # This won't be accurate, because there's only one group.
             grp_slope = self.output_obj.data[integ, 0, :, :]
             if hasattr(self.persistencesat, "dq"):
-                mask = (np.bitwise_and(self.persistencesat.dq,
-                                       dqflags.pixel["DO_NOT_USE"]) > 0)
+                mask = (
+                    np.bitwise_and(self.persistencesat.dq, dqflags.pixel["DO_NOT_USE"]) > 0
+                )  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: Skips and parens removed
created_at: 2025-05-07T15:55:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -255,14 +265,13 @@ def do_all(self):
             # Decays during the time covered by the reset (again, if any)
             # between integrations will be taken care of in the loop
             # over integrations.
-            to_start = (self.output_obj.meta.exposure.start_time
-                        - self.traps_filled.meta.exposure.end_time) * 86400.
-            log.debug("Decay time for previous traps-filled file = %g s",
-                      to_start)
+            to_start = (
+                self.output_obj.meta.exposure.start_time - self.traps_filled.meta.exposure.end_time
+            ) * 86400.0
path: jwst/persistence/persistence.py
user: login: stscirij
body: I actually did this instead:
`            mjd_start = self.output_obj.meta.exposure.start_time`
`            mjd_end = self.traps_filled.meta.exposure.end_time`
`            to_start = (mjd_start - mjd_end) * 86400.0`
created_at: 2025-05-07T15:57:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -605,12 +608,14 @@ def compute_slope(self, integ):
         diff = data[1:, :, :] - data[0:-1, :, :]
         max_diff = diff.max()
         # Larger than any actual data value
-        huge_diff = max(2. * max_diff, 1.e5)
+        huge_diff = max(2.0 * max_diff, 1.0e5)
         del max_diff
 
         # This assumes that the saturation step has already been run.
         # n_sat is a 2-D array of the number of saturated groups per pixel.
-        s_mask = (np.bitwise_and(gdq[1:, :, :], gdqflags["SATURATED"]) > 0)
+        s_mask = (
path: jwst/persistence/persistence.py
user: login: stscirij
body: Fixed
created_at: 2025-05-07T16:00:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,15 +11,20 @@
 log = logging.getLogger(__name__)
 log.setLevel(logging.DEBUG)
 
-SCALEFACTOR = 2.
+SCALEFACTOR = 2.0
 """This factor is to account for the difference in gain for charges freed
 from traps, compared with photon-generated charges.
 """
 
 
-def no_NaN(input_model, fill_value,
-           zap_nan=False, zap_zero=False):
-    """Replace NaNs and/or zeros with a fill value.
+def no_NaN(  # noqa: N802
path: jwst/persistence/persistence.py
user: login: stscirij
body: Fixed
created_at: 2025-05-07T16:01:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,7 +232,7 @@ def do_all(self):
         # rely on that because there might not be an input traps_filled file.
         (det_ny, det_nx) = self.trap_density.data.shape
 
-        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)
+        is_subarray = (shape[-2] != det_ny or shape[-1] != det_nx)  # fmt: skip
path: jwst/persistence/persistence.py
user: login: stscirij
body: Suggestion adopted
created_at: 2025-05-07T16:01:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-10T15:35:29Z
body: ## [Codecov](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) Report
:x: Patch coverage is `58.62069%` with `36 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 77.92%. Comparing base ([`c25698a`](https://app.codecov.io/gh/spacetelescope/jwst/commit/c25698a488141bcb75223469981ab29a2a97a0a9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope)) to head ([`677dce1`](https://app.codecov.io/gh/spacetelescope/jwst/commit/677dce169f4e2eddcbf13de6cd4fe338f7e9447b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope)).
:warning: Report is 682 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?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/persistence/persistence.py](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?src=pr&el=tree&filepath=jwst%2Fpersistence%2Fpersistence.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-andzdC9wZXJzaXN0ZW5jZS9wZXJzaXN0ZW5jZS5weQ==) | 69.44% | [22 Missing :warning: ](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) |
| [jwst/persistence/persistence\_step.py](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?src=pr&el=tree&filepath=jwst%2Fpersistence%2Fpersistence_step.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-andzdC9wZXJzaXN0ZW5jZS9wZXJzaXN0ZW5jZV9zdGVwLnB5) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?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    #9390      +/-   ##
==========================================
- Coverage   77.92%   77.92%   -0.01%     
==========================================
  Files         362      362              
  Lines       36279    36283       +4     
==========================================
+ Hits        28269    28272       +3     
- Misses       8010     8011       +1     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/spacetelescope/jwst/pull/9390?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-05-07T18:41:10Z
body: LGTM - is there a recent regtest run?
author_association: COLLABORATOR
user: login: stscirij
created_at: 2025-05-07T18:54:31Z
body: Regression test run here:
https://github.com/spacetelescope/RegressionTests/actions/runs/14891033067
author_association: CONTRIBUTOR