Repository: spacetelescope/jwst

Issue:
NIRSpec TSO filenames may be missing the slitname
_Issue [JP-3702](https://jira.stsci.edu/browse/JP-3702) was created on JIRA by [Tyler Pauly](https://jira.stsci.edu/secure/ViewProfile.jspa?name=tpauly):_

DMS Ops reported a data product has had its filename change:

<https://stsci.slack.com/archives/C9KF4MVEV/p1722348421168879>
```
I just noticed an issue where a JWST observation that had been included in a DOI was no longer available in the MAST portal. The original observation was:
jw02512-o008_t007_nirspec_f290lp-g395h-s1600a1-sub2048
and the observation that I think is equivalent is:
jw02512-o008_t007_nirspec_f290lp-g395h-sub2048
```

It looks like work completed for JP-3233 may have altered behavior for NIRSpec TSO data - it was not given the new product name generation, and the change to _get_opt_element removed the slit name from the concatenated list of elements. 

Work should include new test coverage of NIRSpec BOTS - this should have been caught by regressions.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/spacetelescope/jwst/pull/8699?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 60.36%. Comparing base [(`f6c4095`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/f6c409555db2b1f49a6dfe497e7bc8df8c07eb35?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) to head [(`2bca6f0`)](https://app.codecov.io/gh/spacetelescope/jwst/commit/2bca6f001d6501a6cde649fe251f54b1983c49b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).
> Report is 2 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #8699      +/-   ##
==========================================
- Coverage   60.38%   60.36%   -0.02%     
==========================================
  Files         372      372              
  Lines       38337    38338       +1     
==========================================
- Hits        23148    23142       -6     
- Misses      15189    15196       +7     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/spacetelescope/jwst/pull/8699?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).

melanieclarke: Regression tests here:
https://plwishmaster.stsci.edu:8081/job/RT/job/JWST-Developers-Pull-Requests/1641/
melanieclarke: Regression tests look correct.  Only NIRSpec associations are affected, and the only change in each case is that the product name now contains the slit, in between the grating and the subarray (if present).

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 47b285664..467b89055 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,6 +17,12 @@ assign_wcs
 - Moved `update_s_region_imaging`, `update_s_region_keyword`, and `wcs_from_footprints`
   into stcal. [#8624]
 
+associations
+------------
+
+- Restored slit name to level 3 product names for NIRSpec BOTS and background
+  fixed slit targets. [#8699]
+
 cube_build
 ----------
 
diff --git a/jwst/associations/lib/dms_base.py b/jwst/associations/lib/dms_base.py
index d3b885174..97882a880 100644
--- a/jwst/associations/lib/dms_base.py
+++ b/jwst/associations/lib/dms_base.py
@@ -765,7 +765,7 @@ class DMSBaseMixin(ACIDMixin):
             else:
                 values.sort(key=str.lower)
                 value = format_list(values)
-                if value not in _EMPTY:
+                if value not in _EMPTY and value not in slit_names:
                     slit_names.append(value)
 
         # Build the string. Sort the elements in order to
@@ -773,9 +773,7 @@ class DMSBaseMixin(ACIDMixin):
         slit_names.sort(key=str.lower)
         slit_name = '-'.join(slit_names)
 
-        if slit_name == '':
-            slit_name = None
-
+        # Slit name may be empty string
         return slit_name
 
     def _get_subarray(self):
diff --git a/jwst/associations/lib/rules_level3_base.py b/jwst/associations/lib/rules_level3_base.py
index b16a444ae..b072f6cbe 100644
--- a/jwst/associations/lib/rules_level3_base.py
+++ b/jwst/associations/lib/rules_level3_base.py
@@ -158,6 +158,10 @@ class DMS_Level3_Base(DMSBaseMixin, Association):
 
         opt_elem = association._get_opt_element()
 
+        slit_name = association._get_slit_name()
+        if slit_name:
+            slit_name = '-' + slit_name
+
         exposure = association._get_exposure()
         if exposure:
             exposure = '-' + exposure
@@ -170,7 +174,7 @@ class DMS_Level3_Base(DMSBaseMixin, Association):
             'jw{program}-{acid}'
             '_{target}'
             '_{instrument}'
-            '_{opt_elem}{subarray}'
+            '_{opt_elem}{slit_name}{subarray}'
         )
         product_name = product_name.format(
             program=association.data['program'],
@@ -178,6 +182,7 @@ class DMS_Level3_Base(DMSBaseMixin, Association):
             target=target,
             instrument=instrument,
             opt_elem=opt_elem,
+            slit_name=slit_name,
             subarray=subarray,
             exposure=exposure
         )
@@ -633,6 +638,10 @@ def dms_product_name_sources(asn):
 
     opt_elem = asn._get_opt_element()
 
+    slit_name = asn._get_slit_name()
+    if slit_name:
+        slit_name = '-' + slit_name
+
     subarray = asn._get_subarray()
     if subarray:
         subarray = '-' + subarray
@@ -641,7 +650,7 @@ def dms_product_name_sources(asn):
         'jw{program}-{acid}'
         '_{source_id}'
         '_{instrument}'
-        '_{opt_elem}{subarray}'
+        '_{opt_elem}{slit_name}{subarray}'
     )
     product_name = format_product(
         product_name_format,
@@ -649,6 +658,7 @@ def dms_product_name_sources(asn):
         acid=asn.acid.id,
         instrument=instrument,
         opt_elem=opt_elem,
+        slit_name=slit_name,
         subarray=subarray,
     )
 
@@ -656,8 +666,10 @@ def dms_product_name_sources(asn):
 
 
 def dms_product_name_nrsfs_sources(asn):
-    """Produce source-based product names for
-       NIRSpec fixed-slit observations.
+    """Produce source-based product names for NIRSpec fixed-slit observations.
+
+    For this mode, the product names have a placeholder for the
+    slit name, to be filled in later by the pipeline.
 
     Parameters
     ---------
@@ -674,10 +686,6 @@ def dms_product_name_nrsfs_sources(asn):
 
     opt_elem = asn._get_opt_element()
 
-    slit_name = asn._get_slit_name()
-    if slit_name:
-        slit_name = '-' + slit_name
-
     subarray = asn._get_subarray()
     if subarray:
         subarray = '-' + subarray


Test patch:
diff --git a/jwst/associations/tests/test_level3_product_names.py b/jwst/associations/tests/test_level3_product_names.py
index 5259bf2a7..c77a14620 100644
--- a/jwst/associations/tests/test_level3_product_names.py
+++ b/jwst/associations/tests/test_level3_product_names.py
@@ -9,7 +9,7 @@ from jwst.associations.tests.helpers import (
     t_path,
 )
 
-from jwst.associations import (AssociationPool, generate)
+from jwst.associations import generate
 from jwst.associations.lib.dms_base import DMSAttrConstraint
 
 
@@ -96,7 +96,7 @@ def test_level3_names(pool_file, global_constraints):
     rules = registry_level3_only(
         global_constraints=global_constraints
     )
-    pool = AssociationPool.read(pool_file)
+    pool = combine_pools(pool_file)
     asns = generate(pool, rules)
     for asn in asns:
         product_name = asn['products'][0]['name']
@@ -110,18 +110,64 @@ def test_level3_names(pool_file, global_constraints):
 
 def test_multiple_optelems(pool_file):
     rules = registry_level3_only()
-    pool = AssociationPool.read(pool_file)
-    asns = generate(pool, rules)
+    pool = combine_pools(pool_file)
+    asns = generate(pool, rules, finalize=False)
     for asn in asns:
         product_name = asn['products'][0]['name']
         if asn['asn_rule'] != 'Asn_Lv3MIRMRS':
             m = re.match(LEVEL3_PRODUCT_NAME_REGEX, product_name)
             assert m is not None
+
+            # there should always be an opt_elem
+            values = ['-'.join(asn.constraints['opt_elem'].found_values)]
+
+            # there may also be an opt_elem2, fixed slit or 2, or a subarray
+            for extra in ['opt_elem2', 'fxd_slit', 'fxd_slit2', 'subarray']:
+
+                # special rules for fixed slit for NRS FS:
+                # it gets a format placeholder instead of the value
+                if asn['asn_rule'] == 'Asn_Lv3NRSFSS':
+                    if extra == 'fxd_slit':
+                        values.append('{slit_name}')
+                        continue
+                    elif extra == 'fxd_slit2':
+                        continue
+
+                try:
+                    value = '-'.join(asn.constraints[extra].found_values)
+                except KeyError:
+                    value = None
+
+                # empty values and subarray = full are not recorded
+                if value not in EMPTY and value != 'full':
+                    values.append(value)
+
+            assert m.groupdict()['opt_elem'] == '-'.join(values)
+
+
+def test_tso3_names():
+    rules = registry_level3_only()
+    tso_pool = t_path('data/pool_021_tso.csv')
+    pool = combine_pools(tso_pool)
+    asns = generate(pool, rules, finalize=False)
+    for asn in asns:
+        product_name = asn['products'][0]['name']
+
+        m = re.match(LEVEL3_PRODUCT_NAME_REGEX, product_name)
+        assert m is not None
+
+        # there should always be an opt_elem
+        values = ['-'.join(asn.constraints['opt_elem'].found_values)]
+
+        # there may also be an opt_elem2, fixed slit or 2, or a subarray
+        for extra in ['opt_elem2', 'fxd_slit', 'fxd_slit2', 'subarray']:
             try:
-                value = '-'.join(asn.constraints['opt_elem2'].found_values)
+                value = '-'.join(asn.constraints[extra].found_values)
             except KeyError:
                 value = None
-            if value in EMPTY:
-                assert '-' not in m.groupdict()['opt_elem']
-            else:
-                assert '-' in m.groupdict()['opt_elem']
+
+            # empty values and subarray = full are not recorded
+            if value not in EMPTY and value != 'full':
+                values.append(value)
+
+        assert m.groupdict()['opt_elem'] == '-'.join(values)