Repository: wright-group/WrightTools

Issue:
quickNd:  channel collapsing broken
If you have a channel with a trivial extra dimension (e.g. shape (1, 25, 25)), and you try to plot with quickNd, the collapse operation fails.  
```
  File "...WrightTools\artists\_quick.py", line 230, in quick2D
    with closing(data.chop(xaxis, yaxis, at=at, verbose=False)) as chopped:
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...WrightTools\data\_data.py", line 501, in chop
    k = k.strip()
        ^^^^^^^
```
The at kwarg for collapsing the trivial axis is constructed by:
```
    collapse = [i for i in range(len(shape)) if shape[i] == 1]
    at = at.copy()
    at.update({c: 0 for c in collapse})
```

this `at` dict, based on array index values, not name:[value, unit], is neither supported nor specified in chop (note: this likely worked prior to 3.4.6, when chop was heavily refactored).  Note other functions support array indexing, such as `Data._from_slice`, or simply `Data.__getitem__`

As an additional issue, chopping resulting from collapsing a dimension will not produce a meaningful constant, so we should neglect these constants from reporting in quickNd as well.



Discussion:

Resolving patch:
diff --git a/WrightTools/__citation__.py b/WrightTools/__citation__.py
--- a/WrightTools/__citation__.py
+++ b/WrightTools/__citation__.py
@@ -6,6 +6,6 @@
 
 here = pathlib.Path(__file__).parent
 
-with here / "CITATION" as f:
+with open(str(here / "CITATION")) as f:
     # remove extra whitespace
-    __citation__ = " ".join(f.read_text().split())
+    __citation__ = " ".join(f.read().split())
diff --git a/WrightTools/artists/_interact.py b/WrightTools/artists/_interact.py
--- a/WrightTools/artists/_interact.py
+++ b/WrightTools/artists/_interact.py
@@ -217,7 +217,9 @@ def interact2D(
     if nsliders < 0:
         raise DimensionalityError(">= 2", data.ndim)
     # TODO: implement aspect; doesn't work currently because of our incorporation of colorbar
-    fig, gs = create_figure(width="single", nrows=7 + nsliders, cols=[1, 1, 1, 1, 1, "cbar"])
+    fig, gs = create_figure(
+        width="single", margin=[0.2, 1, 0.2, 1], nrows=7 + nsliders, cols=[1, 1, 1, 1, 1, "cbar"]
+    )
     plt.get_current_fig_manager().set_window_title(f"interact2D: {data.natural_name}")
     # create axes
     ax0 = plt.subplot(gs[1:6, 0:5])
diff --git a/WrightTools/artists/_quick.py b/WrightTools/artists/_quick.py
--- a/WrightTools/artists/_quick.py
+++ b/WrightTools/artists/_quick.py
@@ -34,7 +34,7 @@ def quick1D(
     autosave=False,
     save_directory=None,
     fname=None,
-    verbose=True
+    verbose=True,
 ):
     """Quickly plot 1D slice(s) of data.
 
@@ -65,14 +65,15 @@ def quick1D(
     list of strings
         List of saved image files (if any).
     """
-    # channel index
     channel_index = wt_kit.get_index(data.channel_names, channel)
     shape = data.channels[channel_index].shape
-    collapse = [i for i in range(len(shape)) if shape[i] == 1]
-    at = at.copy()
-    at.update({c: 0 for c in collapse})
+    # remove dimensions that do not involve the channel
+    channel_slice = [0 if size == 1 else slice(None, None) for size in shape]
+    sliced_constants = [
+        data.axis_expressions[i] for i in range(len(shape)) if not channel_slice[i]
+    ]
     # prepare data
-    with closing(data.chop(axis, at=at, verbose=False)) as chopped:
+    with closing(data._from_slice(channel_slice).chop(axis, at=at, verbose=False)) as chopped:
         # prepare figure
         fig = None
         if len(chopped) > 10:
@@ -132,18 +133,19 @@ def quick1D(
             plt.xticks(rotation=45)
             plt.axvline(0, lw=2, c="k")
             plt.xlim(xi.min(), xi.max())
-            # add constants to title
+            # constants: variable marker lines, title
             ls = []
             for constant in d.constants:
+                if constant.expression in sliced_constants:
+                    # ignore these constants; no relation to the data
+                    continue
                 ls.append(constant.label)
-            title = ", ".join(ls)
-            _title(fig, data.natural_name, subtitle=title)
-            # variable marker lines
-            for constant in d.constants:
                 if constant.units is not None:
                     if axis.units_kind == constant.units_kind:
                         constant.convert(axis.units)
                         plt.axvline(constant.value, color="k", linewidth=4, alpha=0.25)
+            title = ", ".join(ls)
+            _title(fig, data.natural_name, subtitle=title)
             # save --------------------------------------------------------------------------------
             if autosave:
                 if fname:
@@ -175,7 +177,7 @@ def quick2D(
     autosave=False,
     save_directory=None,
     fname=None,
-    verbose=True
+    verbose=True,
 ):
     """Quickly plot 2D slice(s) of data.
 
@@ -223,21 +225,14 @@ def quick2D(
     # channel index
     channel_index = wt_kit.get_index(data.channel_names, channel)
     shape = data.channels[channel_index].shape
-    collapse = [i for i in range(len(shape)) if shape[i] == 1]
-    at = at.copy()
-    at.update({c: 0 for c in collapse})
-    # prepare data
-    with closing(data.chop(xaxis, yaxis, at=at, verbose=False)) as chopped:
-        # colormap
-        # get colormap
-        if cmap is None:
-            if data.channels[channel_index].signed:
-                cmap = "signed"
-            else:
-                cmap = "default"
-            cmap = colormaps[cmap]
-            cmap.set_bad([0.75] * 3, 1.0)
-            cmap.set_under([0.75] * 3, 1.0)
+    # remove axes that are independent of channel
+    channel_slice = [0 if size == 1 else slice(None, None) for size in shape]
+    sliced_constants = [
+        data.axis_expressions[i] for i in range(len(shape)) if not channel_slice[i]
+    ]
+    with closing(
+        data._from_slice(channel_slice).chop(xaxis, yaxis, at=at, verbose=False)
+    ) as chopped:
         # fname
         if fname is None:
             fname = data.natural_name
@@ -344,14 +339,13 @@ def quick2D(
             # add zero lines
             plt.axvline(0, lw=2, c="k")
             plt.axhline(0, lw=2, c="k")
-            # add constants to title
+            # constants: variable marker lines, title
             ls = []
             for constant in d.constants:
+                if constant.expression in sliced_constants:
+                    # ignore these constants; no relation to the data
+                    continue
                 ls.append(constant.label)
-            title = ", ".join(ls)
-            _title(fig, data.natural_name, subtitle=title)
-            # variable marker lines
-            for constant in d.constants:
                 if constant.units is not None:
                     # x axis
                     if xaxis.units_kind == constant.units_kind:
@@ -361,6 +355,8 @@ def quick2D(
                     if yaxis.units_kind == constant.units_kind:
                         constant.convert(yaxis.units)
                         plt.axhline(constant.value, color="k", linewidth=4, alpha=0.25)
+            title = ", ".join(ls)
+            _title(fig, data.natural_name, subtitle=title)
             # colorbar
             cax = plt.subplot(gs[1])
             cbar_ticks = np.linspace(levels.min(), levels.max(), 11)


Test patch:
diff --git a/tests/artists/test_quick1D.py b/tests/artists/test_quick1D.py
--- a/tests/artists/test_quick1D.py
+++ b/tests/artists/test_quick1D.py
@@ -30,10 +30,26 @@ def test_2D():
     wt.artists.quick1D(data)
 
 
+def test_moment_channel():
+    w1 = np.linspace(-2, 2, 51)
+    w2 = np.linspace(-1, 1, 3)
+    w3 = np.linspace(1, 3, 5)
+    signal = np.cos(w1[:, None, None] + w2[None, :, None] + w3[None, None, :])
+    data = wt.data.Data(name="data")
+    data.create_channel("signal", values=signal, signed=True)
+    data.create_variable("w1", values=w1[:, None, None], units="wn", label="1")
+    data.create_variable("w2", values=w2[None, :, None], units="wn", label="2")
+    data.create_variable("w3", values=w3[None, None, :], units="wn", label="3")
+    data.transform("w1", "w2", "w3")
+    data.moment(2, 0, 0)
+    wt.artists.quick1D(data, channel=-1)
+
+
 if __name__ == "__main__":
     import matplotlib.pyplot as plt
 
     plt.close("all")
     test_perovskite()
     test_2D()
+    test_moment_channel()
     plt.show()
diff --git a/tests/artists/test_quick2D.py b/tests/artists/test_quick2D.py
--- a/tests/artists/test_quick2D.py
+++ b/tests/artists/test_quick2D.py
@@ -39,10 +39,26 @@ def test_4D():
     wt.artists.quick2D(data, xaxis=0, yaxis=1)
 
 
+def test_moment_channel():
+    w1 = np.linspace(-2, 2, 51)
+    w2 = np.linspace(-1, 1, 11)
+    w3 = np.linspace(1, 3, 5)
+    signal = np.cos(w1[:, None, None] + w2[None, :, None] + w3[None, None, :])
+    data = wt.data.Data(name="data")
+    data.create_channel("signal", values=signal, signed=True)
+    data.create_variable("w1", values=w1[:, None, None], units="wn", label="1")
+    data.create_variable("w2", values=w2[None, :, None], units="wn", label="2")
+    data.create_variable("w3", values=w3[None, None, :], units="wn", label="3")
+    data.transform("w1", "w2", "w3")
+    data.moment(2, 0, 0)
+    wt.artists.quick2D(data, channel=-1)
+
+
 if __name__ == "__main__":
     import matplotlib.pyplot as plt
 
     plt.close("all")
     test_perovskite()
     test_4D()
+    test_moment_channel()
     plt.show()
diff --git a/tests/data/from_PyCMDS.py b/tests/data/from_PyCMDS.py
--- a/tests/data/from_PyCMDS.py
+++ b/tests/data/from_PyCMDS.py
@@ -162,7 +162,16 @@ def test_centers_tolerance():
 
 
 def test_remote():
-    data = wt.data.from_PyCMDS("https://osf.io/rdn7v/download")
+    i = 0
+    while True:
+        # sometimes doesn't work, so try a few times
+        try:
+            data = wt.data.from_PyCMDS("https://osf.io/rdn7v/download")
+            break
+        except FileNotFoundError as e:
+            if i == 5:
+                raise e
+            i += 1
     assert data.shape == (21, 81)
     assert data.axis_expressions == ("wm", "w2=w1")
     data.close()