Repository: spacetelescope/jdaviz

Issue:
Save composite spectral subset the same as a spatial subset
**Reporter: Jesse Averbukh**

This means that the tree of operations and regions used to compute a composite subset in glue should be accessible to both spectral and spatial subsets. Currently only spatial subsets save this information. This can be used in the Subset Tools plugin to edit spectral subsets. Part of the acceptance criteria is having a "simplify" API which turns the composite spectral subset into a composite subset containing only OR operations (which is simpler to follow and is visually how the spectrum will appear).

 --- 

 DISCLAIMER: This issue was autocreated by the Jdaviz Issue Creation Bot on behalf of the reporter. If any information is incorrect, please contact [Duy Nguyen](https://github.com/duytnguyendtn)

Discussion:
codecov[bot]: ## [Codecov](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) Report
Patch coverage: **`95.42`**% and project coverage change: **`+0.02`** :tada:
> Comparison is base [(`1904937`)](https://codecov.io/gh/spacetelescope/jdaviz/commit/19049377ffb66573516a0a4e77656c4cdbe54683?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) 91.93% compared to head [(`e6195b8`)](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) 91.96%.

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


```diff
@@            Coverage Diff             @@
##             main    #2138      +/-   ##
==========================================
+ Coverage   91.93%   91.96%   +0.02%     
==========================================
  Files         143      146       +3     
  Lines       15781    15939     +158     
==========================================
+ Hits        14509    14659     +150     
- Misses       1272     1280       +8     
```


| [Impacted Files](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope) | Coverage Δ | |
|---|---|---|
| [jdaviz/configs/imviz/plugins/viewers.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy92aWV3ZXJzLnB5) | `91.13% <ø> (ø)` | |
| [jdaviz/core/events.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvcmUvZXZlbnRzLnB5) | `94.61% <86.66%> (-0.58%)` | :arrow_down: |
| [jdaviz/configs/imviz/plugins/compass/compass.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy9jb21wYXNzL2NvbXBhc3MucHk=) | `93.10% <93.33%> (-0.23%)` | :arrow_down: |
| [jdaviz/app.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2FwcC5weQ==) | `93.39% <95.23%> (-0.02%)` | :arrow_down: |
| [...nfigs/imviz/plugins/rotate\_canvas/rotate\_canvas.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy9yb3RhdGVfY2FudmFzL3JvdGF0ZV9jYW52YXMucHk=) | `95.58% <95.58%> (ø)` | |
| [jdaviz/configs/cubeviz/helper.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvY3ViZXZpei9oZWxwZXIucHk=) | `98.85% <100.00%> (+0.05%)` | :arrow_up: |
| [...nfigs/cubeviz/plugins/tests/test\_cubeviz\_helper.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvY3ViZXZpei9wbHVnaW5zL3Rlc3RzL3Rlc3RfY3ViZXZpel9oZWxwZXIucHk=) | `100.00% <100.00%> (ø)` | |
| [jdaviz/configs/imviz/plugins/\_\_init\_\_.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [...iz/configs/imviz/plugins/rotate\_canvas/\_\_init\_\_.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy9yb3RhdGVfY2FudmFzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [.../plugins/rotate\_canvas/tests/test\_rotate\_canvas.py](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope#diff-amRhdml6L2NvbmZpZ3MvaW12aXovcGx1Z2lucy9yb3RhdGVfY2FudmFzL3Rlc3RzL3Rlc3Rfcm90YXRlX2NhbnZhcy5weQ==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/spacetelescope/jdaviz/pull/2138?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=spacetelescope).

rosteen: I think it only happens if you also have spatial subsets defined.
kecnry: I'm guessing that isn't directly related to this PR (can you check if it happens on main)?  Maybe we can consider out-of-scope for the review here and file an issue/ticket?
rosteen: Ahh, right, this only implements the non-simplified output. The bug is also present on main, I'll open a separate ticket for that. My other comment applies to main as well then, I'll approve this. 

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index eedf0811..aab6d86b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,6 +6,8 @@ New Features
 
 * Model fitting results are logged in a table within the plugin [#2093].
 
+* Auto-identify a configuration/helper for a given data file [#2124]
+
 Cubeviz
 ^^^^^^^
 
@@ -32,7 +34,7 @@ API Changes
 -----------
 
 - Add ``get_subsets()`` method to app level to centralize subset information
-  retrieval. [#2087, #2116]
+  retrieval. [#2087, #2116, #2138]
 
 Cubeviz
 ^^^^^^^
diff --git a/docs/dev/release.rst b/docs/dev/release.rst
index c9667764..80b21123 100644
--- a/docs/dev/release.rst
+++ b/docs/dev/release.rst
@@ -222,9 +222,18 @@ You can do a release from your fork directly without a clean code check-out.
      Specviz2d
      ^^^^^^^^^
 
-#. Commit your changes of the, uh, change log with a message, "Back to development: A.B.dev"
+#. Commit your changes of the, uh, change log with a message, "Back to development: A.C.dev"
    and push directly to ``main``.
 
+#. For this commit, if you are doing a "major" release, also do this so ``setuptools-scm``
+   is able to report the dev version properly. This is needed because it cannot grab
+   the new release tag from a release branch:
+
+.. code-block:: bash
+
+     git tag -a vA.C.dev -m "Back to development: A.C.dev"
+     git push upstream vA.C.dev
+
 #. Follow procedures for :ref:`release-milestones` and :ref:`release-labels`.
 
 #. For your own sanity unrelated to the release, grab the new tag for your fork:
@@ -626,6 +635,14 @@ You can create a clean checkout as follows (requires
      git add CHANGES.rst
      git commit -m "Back to development: A.B.dev"
 
+#. For this commit, if you are doing a "major" release, also do this so ``setuptools-scm``
+   is able to report the dev version properly. This is needed because it cannot grab
+   the new release tag from a release branch:
+
+.. code-block:: bash
+
+     git tag -a vA.B.dev -m "Back to development: A.B.dev"
+
 #. Push out the updated code and tag. If applicable, change ``origin`` to point to
    the remote that points to the repository being released:
 
@@ -633,6 +650,7 @@ You can create a clean checkout as follows (requires
 
      git push origin main
      git push origin vX.Y.Z
+     git push origin vA.B.dev
 
 #. Go to `Releases on GitHub <https://github.com/spacetelescope/jdaviz/releases>`_
    and `create a new GitHub release <https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository>`_
diff --git a/jdaviz/app.py b/jdaviz/app.py
index 37568b3c..81d04123 100644
--- a/jdaviz/app.py
+++ b/jdaviz/app.py
@@ -828,7 +828,7 @@ class Application(VuetifyTemplate, HubListener):
         return regions
 
     def get_subsets(self, subset_name=None, spectral_only=False,
-                    spatial_only=False, object_only=False):
+                    spatial_only=False, object_only=False, simplify_spectral=True):
         """
         Returns all branches of glue subset tree in the form that subset plugin can recognize.
 
@@ -843,6 +843,8 @@ class Application(VuetifyTemplate, HubListener):
         object_only : bool
             Return only object relevant information and
             leave out the region class name and glue_state.
+        simplify_spectral : bool
+            Return a composite spectral subset collapsed to a simplified SpectralRegion.
 
         Returns
         -------
@@ -861,26 +863,36 @@ class Application(VuetifyTemplate, HubListener):
             if isinstance(subset.subset_state, CompositeSubsetState):
                 # Region composed of multiple ROI or Range subset
                 # objects that must be traversed
-                subset_region = self.get_sub_regions(subset.subset_state)
+                subset_region = self.get_sub_regions(subset.subset_state, simplify_spectral)
             elif isinstance(subset.subset_state, RoiSubsetState):
                 # 3D regions represented as a dict including an
                 # AstropyRegion object if possible
                 subset_region = self._get_roi_subset_definition(subset.subset_state)
             elif isinstance(subset.subset_state, RangeSubsetState):
                 # 2D regions represented as SpectralRegion objects
-                subset_region = self._get_range_subset_bounds(subset.subset_state)
+                subset_region = self._get_range_subset_bounds(subset.subset_state,
+                                                              simplify_spectral)
             else:
                 # subset.subset_state can be an instance of MaskSubsetState
                 # or something else we do not know how to handle
                 all_subsets[label] = None
                 continue
 
-            if isinstance(subset_region, SpectralRegion):
+            # Is the subset spectral or spatial?
+            is_spectral = self._is_subset_spectral(subset_region)
+
+            # Remove duplicate spectral regions
+            if is_spectral and isinstance(subset_region, SpectralRegion):
                 subset_region = self._remove_duplicate_bounds(subset_region)
+            elif is_spectral:
+                subset_region = self._remove_duplicate_bounds_in_dict(subset_region)
 
-            if spectral_only and isinstance(subset_region, SpectralRegion):
-                all_subsets[label] = subset_region
-            elif spatial_only and not isinstance(subset_region, SpectralRegion):
+            if spectral_only and is_spectral:
+                if object_only and not simplify_spectral:
+                    all_subsets[label] = [reg['region'] for reg in subset_region]
+                else:
+                    all_subsets[label] = subset_region
+            elif spatial_only and not is_spectral:
                 if object_only:
                     all_subsets[label] = [reg['region'] for reg in subset_region]
                 else:
@@ -899,6 +911,30 @@ class Application(VuetifyTemplate, HubListener):
         else:
             return all_subsets
 
+    def _remove_duplicate_bounds_in_dict(self, subset_region):
+        new_subset_region = []
+        for elem in subset_region:
+            if not new_subset_region:
+                new_subset_region.append(elem)
+                continue
+            unique = True
+            for elem2 in new_subset_region:
+                if (elem['region'].lower == elem2['region'].lower and
+                        elem['region'].upper == elem2['region'].upper and
+                        elem['glue_state'] == elem2['glue_state']):
+                    unique = False
+            if unique:
+                new_subset_region.append(elem)
+        return new_subset_region
+
+    def _is_subset_spectral(self, subset_region):
+        if isinstance(subset_region, SpectralRegion):
+            return True
+        elif isinstance(subset_region, list) and len(subset_region) > 0:
+            if isinstance(subset_region[0]['region'], SpectralRegion):
+                return True
+        return False
+
     def _remove_duplicate_bounds(self, spec_regions):
         regions_no_dups = None
 
@@ -909,7 +945,7 @@ class Application(VuetifyTemplate, HubListener):
                 regions_no_dups += region
         return regions_no_dups
 
-    def _get_range_subset_bounds(self, subset_state):
+    def _get_range_subset_bounds(self, subset_state, simplify_spectral=True):
         # TODO: Use global display units
         # units = dc[0].data.coords.spectral_axis.unit
         viewer = self.get_viewer(self._jdaviz_helper. _default_spectrum_viewer_reference_name)
@@ -920,7 +956,14 @@ class Application(VuetifyTemplate, HubListener):
             units = data[0].spectral_axis.unit
         else:
             raise ValueError("Unable to find spectral axis units")
-        return SpectralRegion(subset_state.lo * units, subset_state.hi * units)
+
+        spec_region = SpectralRegion(subset_state.lo * units, subset_state.hi * units)
+        if not simplify_spectral:
+            return [{"name": subset_state.__class__.__name__,
+                     "glue_state": subset_state.__class__.__name__,
+                     "region": spec_region,
+                     "subset_state": subset_state}]
+        return spec_region
 
     def _get_roi_subset_definition(self, subset_state):
         _around_decimals = 6
@@ -946,17 +989,17 @@ class Application(VuetifyTemplate, HubListener):
 
         return [{"name": subset_state.roi.__class__.__name__,
                  "glue_state": subset_state.__class__.__name__,
-                 "region": roi_as_region}]
+                 "region": roi_as_region,
+                 "subset_state": subset_state}]
 
-    def get_sub_regions(self, subset_state):
+    def get_sub_regions(self, subset_state, simplify_spectral=True):
 
         if isinstance(subset_state, CompositeSubsetState):
             if subset_state and hasattr(subset_state, "state2") and subset_state.state2:
-                one = self.get_sub_regions(subset_state.state1)
-                two = self.get_sub_regions(subset_state.state2)
+                one = self.get_sub_regions(subset_state.state1, simplify_spectral)
+                two = self.get_sub_regions(subset_state.state2, simplify_spectral)
 
-                if (isinstance(one, list) and "glue_state" in one[0] and
-                        one[0]["glue_state"] == "RoiSubsetState"):
+                if isinstance(one, list) and "glue_state" in one[0]:
                     one[0]["glue_state"] = subset_state.__class__.__name__
 
                 if isinstance(subset_state.state2, InvertState):
@@ -1014,7 +1057,7 @@ class Application(VuetifyTemplate, HubListener):
             else:
                 # This gets triggered in the InvertState case where state1
                 # is an object and state2 is None
-                return self.get_sub_regions(subset_state.state1)
+                return self.get_sub_regions(subset_state.state1, simplify_spectral)
         elif subset_state is not None:
             # This is the leaf node of the glue subset state tree where
             # a subset_state is either ROI or Range.
@@ -1022,7 +1065,7 @@ class Application(VuetifyTemplate, HubListener):
                 return self._get_roi_subset_definition(subset_state)
 
             elif isinstance(subset_state, RangeSubsetState):
-                return self._get_range_subset_bounds(subset_state)
+                return self._get_range_subset_bounds(subset_state, simplify_spectral)
 
     def add_data(self, data, data_label=None, notify_done=True):
         """
diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py
index e1e317ba..7caa2925 100644
--- a/jdaviz/core/data_formats.py
+++ b/jdaviz/core/data_formats.py
@@ -2,12 +2,22 @@ import os
 import pathlib
 
 import astropy.io
+from astropy.io import registry, fits
+from astropy.nddata import CCDData
+from astropy.wcs import WCS
+
 from specutils.io.registers import identify_spectrum_format
-from specutils import SpectrumList
+from specutils import Spectrum1D, SpectrumList, SpectrumCollection
+from stdatamodels import asdf_in_fits
 
 from jdaviz.core.config import list_configurations
 
-__all__ = ['guess_dimensionality', 'get_valid_format', 'identify_data']
+__all__ = [
+    'guess_dimensionality',
+    'get_valid_format',
+    'identify_data',
+    'identify_helper'
+]
 
 # create a default file format to configuration mapping
 default_mapping = {'JWST x1d': 'specviz', 'JWST s2d': 'specviz2d',
@@ -112,3 +122,153 @@ def identify_data(filename, current=None):
         raise ValueError('Mismatch between input file format and loaded configuration.')
 
     return valid_format, config
+
+
+def _get_wcs(filename, header):
+    """
+    Get gwcs.wcs.WCS or astropy.wcs.WCS from FITS file.
+    """
+    try:
+        with asdf_in_fits.open(filename) as af:
+            wcs = af.tree['meta']['wcs']
+
+    # if the file doesn't have ASDF-in-FITS, then
+    # the 'meta' key doesn't exist, yielding a KeyError:
+    except KeyError:
+        # fall back on using astropy WCS:
+        wcs = WCS(header)
+
+    return wcs
+
+
+def identify_helper(filename, ext=1):
+    """
+    Guess the appropriate viz helper for a data file.
+
+    Parameters
+    ----------
+    filename : str (path-like)
+        Name for a local data file.
+    ext : int
+        Extension from the FITS file.
+
+    Returns
+    -------
+    helper_name : str
+        Name of the best-guess helper for ``filename``.
+    """
+    supported_dtypes = [
+        Spectrum1D,
+        SpectrumList,
+        SpectrumCollection,
+        CCDData
+    ]
+
+    if filename.lower().endswith('asdf'):
+        # ASDF files are only supported in jdaviz for
+        # Roman WFI 2D images, so suggest imviz:
+        return 'imviz'
+
+    header = fits.getheader(filename, ext=ext)
+    data = fits.getdata(filename, ext=ext)
+    wcs = _get_wcs(filename, header)
+    has_spectral_axis = 'spectral' in wcs.world_axis_object_classes
+
+    n_axes = (
+        int(has_spectral_axis) +
+
+        sum([component[0] in ['celestial', 'angle']
+             for component in wcs.world_axis_object_components]) -
+
+        # remove any slit_frame axis from the count
+        (0 if not hasattr(wcs, 'available_frames') else
+         int('slit_frame' in wcs.available_frames))
+    )
+
+    # use astropy to recognize some data formats:
+    possible_formats = {}
+    for cls in supported_dtypes:
+        fmt = registry.identify_format(
+            'read', cls, filename, None, {}, {}
+        )
+        if fmt:
+            possible_formats[cls] = fmt
+
+    # If CCDData is the only match:
+    if len(possible_formats) == 1:
+        only_key, only_value = possible_formats.popitem()
+        if only_key == CCDData:
+            # could be 2D spectrum or 2D image. break tie with WCS:
+            if has_spectral_axis:
+                if n_axes > 1:
+                    return 'specviz2d'
+                return 'specviz'
+            elif not isinstance(data, fits.BinTableHDU):
+                return 'imviz'
+
+    # Ensure specviz is chosen when ``data`` is a table or recarray
+    # and there's a "known" spectral column name:
+    if isinstance(data, (fits.BinTableHDU, fits.fitsrec.FITS_rec)):
+        # now catch spectra in FITS tables, looking for
+        # columns with "wave" or "flux" in the names:
+        table_columns = [getattr(col, 'name', col).lower() for col in data.columns]
+
+        # these are "known" prefixes for column names
+        # in FITS tables of spectral observations
+        known_spectral_columns = [
+            'wave',
+            'flux'
+        ]
+
+        # this list of bools indicates any
+        # spectral column names found:
+        found_spectral_columns = [
+            found_col.startswith(known_col)
+            for known_col in known_spectral_columns
+            for found_col in table_columns
+        ]
+
+        # if at least one spectral column is found:
+        if sum(found_spectral_columns):
+            return 'specviz'
+
+    # If the data could be spectral:
+    for cls in [Spectrum1D, SpectrumList]:
+        if cls in possible_formats.keys():
+            recognized_spectrum_format = possible_formats[cls][0].lower()
+
+            # first catch known JWST spectrum types:
+            if (n_axes == 3 and
+                    recognized_spectrum_format.find('s3d') > -1):
+                return 'cubeviz'
+            elif (n_axes == 2 and
+                  recognized_spectrum_format.find('x1d') > -1):
+                return 'specviz'
+
+            # we intentionally don't choose specviz2d for
+            # data recognized as 's2d' as we did with the cases above,
+            # because 2D data products could be 2D spectra *or* 2D images
+            # that the registry recognizes as s2d.
+
+            # Use WCS to break the tie below:
+            elif n_axes == 2:
+                if has_spectral_axis:
+                    return 'specviz2d'
+                return 'imviz'
+
+            elif n_axes == 1:
+                return 'specviz'
+
+    try:
+        # try using the specutils registry:
+        valid_format, config = identify_data(filename)
+        return config
+    except ValueError:
+        # if file type not recognized:
+        pass
+
+    if n_axes == 2 and not has_spectral_axis:
+        # at this point, non-spectral 2D data are likely images:
+        return 'imviz'
+
+    raise ValueError(f"No helper could be auto-identified for {filename}.")


Test patch:
diff --git a/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py b/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
index 49872a1e..2ccd36d9 100644
--- a/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
+++ b/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
@@ -114,3 +114,20 @@ def test_indicator_settings(cubeviz_helper, spectrum1d_cube):
 
     sl.show_wavelength = False
     assert indicator.label.visible is False
+
+
+@pytest.mark.filterwarnings('ignore:No observer defined on WCS')
+def test_init_slice(cubeviz_helper, spectrum1d_cube):
+    cubeviz_helper.load_data(spectrum1d_cube, data_label='test')
+
+    fv = cubeviz_helper.app.get_viewer('flux-viewer')
+    sl = cubeviz_helper.plugins['Slice']
+    assert sl.slice == 1
+    assert fv.state.slices == (0, 0, 1)
+
+    # make sure adding new data doesn't revert slice to 0
+    mm = cubeviz_helper.plugins['Moment Maps']
+    mm.calculate_moment(add_data=True)
+
+    assert sl.slice == 1
+    assert fv.state.slices == (0, 0, 1)
diff --git a/jdaviz/core/tests/test_data_menu.py b/jdaviz/core/tests/test_data_menu.py
index 24863031..c48e5f3e 100644
--- a/jdaviz/core/tests/test_data_menu.py
+++ b/jdaviz/core/tests/test_data_menu.py
@@ -1,6 +1,30 @@
+import pytest
 import numpy as np
 
 from glue.core.roi import XRangeROI
+from astropy.utils.data import download_file
+
+from jdaviz.core.data_formats import identify_helper
+
+# URIs to example JWST/HST files on MAST, and their
+# corresponding jdaviz helpers:
+example_uri_helper = [
+     ['mast:HST/product/id4301ouq_drz.fits', 'imviz'],
+     ['mast:HST/product/ldq601030_x1dsum.fits', 'specviz'],
+     ['mast:HST/product/o4xw01dkq_flt.fits', 'specviz2d'],
+     ['mast:JWST/product/jw01324-o001_s00094_niriss_f200w-gr150c-gr150r_x1d.fits',
+      'specviz'],
+     ['mast:JWST/product/jw01324-o006_s00005_nirspec_f100lp-g140h_s2d.fits',
+      'specviz2d'],
+     ['mast:JWST/product/jw01345-o001_t021_nircam_clear-f200w_i2d.fits', 'imviz'],
+     ['mast:JWST/product/jw01373-o028_t001_nirspec_g395h-f290lp_s3d.fits',
+      'cubeviz'],
+     ['mast:JWST/product/jw01373-o031_t007_miri_ch1-shortmediumlong_s3d.fits',
+      'cubeviz'],
+     ['mast:JWST/product/jw01783-o004_t008_nircam_clear-f444w_i2d.fits', 'imviz'],
+     ['mast:JWST/product/jw02732-o004_t004_miri_ch1-shortmediumlong_x1d.fits',
+      'specviz']
+]
 
 
 def test_data_menu_toggles(specviz_helper, spectrum1d):
@@ -75,3 +99,27 @@ def test_visibility_toggle(imviz_helper):
                             visible=True)
     assert iv.layers[0].visible is True
     assert po.stretch_preset.value == 90
+
+
+@pytest.mark.remote_data
+@pytest.mark.filterwarnings(r"ignore::astropy.wcs.wcs.FITSFixedWarning")
+@pytest.mark.parametrize(
+    "uri, expected_helper", example_uri_helper
+)
+def test_auto_config_detection(uri, expected_helper):
+    url = f'https://mast.stsci.edu/api/v0.1/Download/file/?uri={uri}'
+    fn = download_file(url, cache=True)
+    helper_name = identify_helper(fn)
+    assert helper_name == expected_helper
+
+
+@pytest.mark.remote_data
+@pytest.mark.filterwarnings(r"ignore::astropy.wcs.wcs.FITSFixedWarning")
+def test_auto_config_manga():
+    # Check that MaNGA cubes go to cubeviz. This file is
+    # originally from
+    # https://data.sdss.org/sas/dr14/manga/spectro/redux/v2_1_2/7495/stack/manga-7495-12704-LOGCUBE.fits.gz
+    URL = 'https://stsci.box.com/shared/static/28a88k1qfipo4yxc4p4d40v4axtlal8y.fits'
+    fn = download_file(URL, cache=True)
+    helper_name = identify_helper(fn)
+    assert helper_name == 'cubeviz'
diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py
index 40cda42b..28f5fbeb 100644
--- a/jdaviz/tests/test_subsets.py
+++ b/jdaviz/tests/test_subsets.py
@@ -322,34 +322,39 @@ def test_composite_region_from_subset_3d(cubeviz_helper):
     viewer.apply_roi(CircularROI(xc=25, yc=25, radius=5))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     circle1 = CirclePixelRegion(center=PixCoord(x=25, y=25), radius=5)
-    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1}
+    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(RectangularROI(25, 30, 25, 30))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     rectangle1 = RectanglePixelRegion(center=PixCoord(x=27.5, y=27.5),
                                       width=5, height=5, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1}
+    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = OrMode
     viewer.apply_roi(EllipticalROI(30, 30, 3, 6))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30),
                                   width=3, height=6, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'OrState', 'region': ellipse1}
+    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'OrState', 'region': ellipse1,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = AndMode
     viewer.apply_roi(RectangularROI(20, 25, 20, 25))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     rectangle2 = RectanglePixelRegion(center=PixCoord(x=22.5, y=22.5),
                                       width=5, height=5, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndState', 'region': rectangle2}
+    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndState', 'region': rectangle2,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(CircularROI(xc=21, yc=24, radius=1))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     circle2 = CirclePixelRegion(center=PixCoord(x=21, y=24), radius=1)
-    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'AndNotState', 'region': circle2}
+    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'AndNotState', 'region': circle2,
+                       'subset_state': reg[-1]['subset_state']}
 
 
 def test_composite_region_with_consecutive_and_not_states(cubeviz_helper):
@@ -362,21 +367,24 @@ def test_composite_region_with_consecutive_and_not_states(cubeviz_helper):
     viewer.apply_roi(CircularROI(xc=25, yc=25, radius=5))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     circle1 = CirclePixelRegion(center=PixCoord(x=25, y=25), radius=5)
-    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1}
+    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(RectangularROI(25, 30, 25, 30))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     rectangle1 = RectanglePixelRegion(center=PixCoord(x=27.5, y=27.5),
                                       width=5, height=5, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1}
+    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(EllipticalROI(30, 30, 3, 6))
     reg = cubeviz_helper.app.get_subsets("Subset 1")
     ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30),
                                   width=3, height=6, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1}
+    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1,
+                       'subset_state': reg[-1]['subset_state']}
 
     regions_list = cubeviz_helper.app.get_subsets("Subset 1", object_only=True)
     assert len(regions_list) == 3
@@ -400,24 +408,65 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs):
     viewer.apply_roi(CircularROI(xc=5, yc=5, radius=2))
     reg = imviz_helper.app.get_subsets("Subset 1")
     circle1 = CirclePixelRegion(center=PixCoord(x=5, y=5), radius=2)
-    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1}
+    assert reg[-1] == {'name': 'CircularROI', 'glue_state': 'RoiSubsetState', 'region': circle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     imviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(RectangularROI(2, 4, 2, 4))
     reg = imviz_helper.app.get_subsets("Subset 1")
     rectangle1 = RectanglePixelRegion(center=PixCoord(x=3, y=3),
                                       width=2, height=2, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1}
+    assert reg[-1] == {'name': 'RectangularROI', 'glue_state': 'AndNotState', 'region': rectangle1,
+                       'subset_state': reg[-1]['subset_state']}
 
     imviz_helper.app.session.edit_subset_mode.mode = AndNotMode
     viewer.apply_roi(EllipticalROI(3, 3, 3, 6))
     reg = imviz_helper.app.get_subsets("Subset 1")
     ellipse1 = EllipsePixelRegion(center=PixCoord(x=3, y=3),
                                   width=3, height=6, angle=0.0 * u.deg)
-    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1}
+    assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1,
+                       'subset_state': reg[-1]['subset_state']}
 
 
 def test_with_invalid_subset_name(cubeviz_helper):
     subset_name = "Test"
     with pytest.raises(ValueError, match=f'{subset_name} not in '):
         cubeviz_helper.app.get_subsets(subset_name=subset_name)
+
+
+def test_composite_region_from_subset_2d(specviz_helper, spectrum1d):
+    specviz_helper.load_spectrum(spectrum1d)
+    viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name)
+    viewer.apply_roi(XRangeROI(6000, 7000))
+    reg = specviz_helper.app.get_subsets("Subset 1", simplify_spectral=False)
+    subset1 = SpectralRegion(6000 * spectrum1d.spectral_axis.unit,
+                             7000 * spectrum1d.spectral_axis.unit)
+    assert reg[-1]['region'].lower == subset1.lower and reg[-1]['region'].upper == subset1.upper
+    assert reg[-1]['glue_state'] == 'RangeSubsetState'
+
+    specviz_helper.app.session.edit_subset_mode.mode = AndNotMode
+
+    viewer.apply_roi(XRangeROI(6500, 6800))
+    reg = specviz_helper.app.get_subsets("Subset 1", simplify_spectral=False)
+    subset1 = SpectralRegion(6500 * spectrum1d.spectral_axis.unit,
+                             6800 * spectrum1d.spectral_axis.unit)
+    assert reg[-1]['region'].lower == subset1.lower and reg[-1]['region'].upper == subset1.upper
+    assert reg[-1]['glue_state'] == 'AndNotState'
+
+    specviz_helper.app.session.edit_subset_mode.mode = OrMode
+
+    viewer.apply_roi(XRangeROI(7200, 7800))
+    reg = specviz_helper.app.get_subsets("Subset 1", simplify_spectral=False)
+    subset1 = SpectralRegion(7200 * spectrum1d.spectral_axis.unit,
+                             7800 * spectrum1d.spectral_axis.unit)
+    assert reg[-1]['region'].lower == subset1.lower and reg[-1]['region'].upper == subset1.upper
+    assert reg[-1]['glue_state'] == 'OrState'
+
+    specviz_helper.app.session.edit_subset_mode.mode = AndMode
+
+    viewer.apply_roi(XRangeROI(6800, 7500))
+    reg = specviz_helper.app.get_subsets("Subset 1", simplify_spectral=False)
+    subset1 = SpectralRegion(6800 * spectrum1d.spectral_axis.unit,
+                             7500 * spectrum1d.spectral_axis.unit)
+    assert reg[-1]['region'].lower == subset1.lower and reg[-1]['region'].upper == subset1.upper
+    assert reg[-1]['glue_state'] == 'AndState'