Repository: pyfar/pyfar (python)

Issue:
creshape and cflatten for Signal class
It would be nice if Signal hat a channel_reshape `creshape()` and channel_flatten function that return copies of the signal with another channel layout. Implementation will be analogous to np.flatten and np.reshape, but the last dimension (n_samples, n_bins) can not be changed.

A use case are the private plot functions (_line.*) and Interaction that require 2D arrays


Pull request description:
added flatten and reshape method for signal
### Which issue(s) are closed by this pull request?

Closes #63 

### Changes proposed in this pull request:

- added flatten and reshape methods for signal
- added test for new methods

Patch:
diff --git a/pyfar/signal.py b/pyfar/signal.py
index 5566681c..b4432056 100644
--- a/pyfar/signal.py
+++ b/pyfar/signal.py
@@ -309,6 +309,68 @@ class Signal(Audio):
         """
         return self._data.shape[:-1]
 
+    def reshape(self, newshape):
+        """
+        Return reshaped copy of the signal.
+
+        Parameters
+        ----------
+        newshape : int, tuple
+            new cshape of the signal. One entry of newshape dimension can be
+            -1. In this case, the value is inferred from the remaining
+            dimensions.
+
+        Returns
+        -------
+        reshaped : Signal
+            reshaped copy of the signal.
+
+        Note
+        ----
+        The number of samples and frequency bin always remains the same.
+
+        """
+
+        # check input
+        if not isinstance(newshape, int) and not isinstance(newshape, tuple):
+            raise ValueError("newshape must be an integer or tuple")
+
+        if isinstance(newshape, int):
+            newshape = (newshape, )
+
+        # reshape
+        reshaped = utils.copy(self)
+        length_last_dimension = reshaped._data.shape[-1]
+        try:
+            reshaped._data = reshaped._data.reshape(
+                newshape + (length_last_dimension, ))
+        except ValueError:
+            if np.prod(newshape) != np.prod(self.cshape):
+                raise ValueError((f"Can not reshape signal of cshape "
+                                  f"{self.cshape} to {newshape}"))
+
+        return reshaped
+
+    def flatten(self):
+        """Return flattened copy of the signal.
+
+        Returns
+        -------
+        flat : Signal
+            Flattened copy of signal with
+            `flat.cshape = np.prod(signal.cshape)`
+
+        Note
+        ----
+        The number of samples and frequency bins always remains the same, e.g.,
+        a signal of `cshape=(4,3)` and `n_samples=512` will have
+        `chsape=(12, )` and `n_samples=512` after flattening.
+
+        """
+        newshape = int(np.prod(self.cshape))
+
+        return self.reshape(newshape)
+
     @property
     def comment(self):
         """Get comment."""


Test patch:
diff --git a/tests/test_signal.py b/tests/test_signal.py
index 7d101147..7039fbd0 100644
--- a/tests/test_signal.py
+++ b/tests/test_signal.py
@@ -344,6 +344,57 @@ def test_find_nearest_frequency():
     npt.assert_allclose(actual, expected)
 
 
+def test_reshape():
+
+    # test reshape with tuple
+    signal_in = Signal(np.random.rand(6, 256), 44100)
+    signal_out = signal_in.reshape((3, 2))
+    npt.assert_allclose(signal_in._data.reshape(3, 2, -1), signal_out._data)
+    assert id(signal_in) != id(signal_out)
+
+    signal_out = signal_in.reshape((3, -1))
+    npt.assert_allclose(signal_in._data.reshape(3, 2, -1), signal_out._data)
+    assert id(signal_in) != id(signal_out)
+
+    # test reshape with int
+    signal_in = Signal(np.random.rand(3, 2, 256), 44100)
+    signal_out = signal_in.reshape(6)
+    npt.assert_allclose(signal_in._data.reshape(6, -1), signal_out._data)
+    assert id(signal_in) != id(signal_out)
+
+
+def test_reshape_exceptions():
+    signal_in = Signal(np.random.rand(6, 256), 44100)
+    signal_out = signal_in.reshape((3, 2))
+    npt.assert_allclose(signal_in._data.reshape(3, 2, -1), signal_out._data)
+    # test assertion for non-tuple input
+    with pytest.raises(ValueError):
+        signal_out = signal_in.reshape([3, 2])
+
+    # test assertion for wrong dimension
+    with pytest.raises(ValueError, match='Can not reshape signal of cshape'):
+        signal_out = signal_in.reshape((3, 4))
+
+
+def test_flatten():
+
+    # test 2D signal (flatten should not change anything)
+    x = np.random.rand(2, 256)
+    signal_in = Signal(x, 44100)
+    signal_out = signal_in.flatten()
+
+    npt.assert_allclose(signal_in._data, signal_out._data)
+    assert id(signal_in) != id(signal_out)
+
+    # test 3D signal
+    x = np.random.rand(3, 2, 256)
+    signal_in = Signal(x, 44100)
+    signal_out = signal_in.flatten()
+
+    npt.assert_allclose(signal_in._data.reshape((6, -1)), signal_out._data)
+    assert id(signal_in) != id(signal_out)
+
+
 @pytest.fixture
 def sine():
     """Generate a sine signal with f = 440 Hz and sampling_rate = 44100 Hz.