Repository: pyfar/pyfar (python)

Issue:
Filtering `Signal` alters its `cshape`
## General

* pyfar version: 0.6.6
* Python version: 3.10.12
* Operating System: Ubuntu LTS 22.04.4
* Did you install pyfar via pip: yes

## Description
Filtering a `Signal` changes its `cshape`, if one dimension is `1`. I don't want my shapes being touched. Worse, it leads to incorrect broadcasting down the line.

## What I Did

MWE
``` python
#!/usr/bin/env python3

import numpy as np
import pyfar


sig = pyfar.Signal(np.zeros((4,1,10)), 1)
out = pyfar.dsp.filter.butterworth(sig, 2, 0.1)

print(sig)
print(out)
print(sig-out)
```
Output
``` bash
time domain energy Signal:
(4, 1) channels with 10 samples @ 1 Hz sampling rate and none FFT normalization

time domain energy Signal:
(4,) channels with 10 samples @ 1 Hz sampling rate and none FFT normalization

freq domain energy Signal:
(4, 4) channels with 10 samples @ 1 Hz sampling rate and none FFT normalization
```

Pull request description:
Change squeeze in filter class to keep cshape of input signal
I changed the squeeze in ``Filter.process`` so it doesn't squeeze any of the input signal's channel dimensions.

Closes #627 

I would do separate PRs for gammatone class and the dsp-functions if changes are necessary.

Patch:
diff --git a/pyfar/classes/filter.py b/pyfar/classes/filter.py
index 6a3b709f..4583567d 100644
--- a/pyfar/classes/filter.py
+++ b/pyfar/classes/filter.py
@@ -247,7 +247,12 @@ class Filter(object):
 
         # prepare output signal
         filtered_signal = deepcopy(signal)
-        filtered_signal.time = np.squeeze(filtered_signal_data)
+
+        # squeeze first dimension if there is only one filter channel
+        if self.n_channels == 1:
+            filtered_signal.time = np.squeeze(filtered_signal_data, axis=0)
+        else:
+            filtered_signal.time = filtered_signal_data
 
         return filtered_signal
 
diff --git a/requirements_dev.txt b/requirements_dev.txt
index b647122b..8d9cc098 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -4,7 +4,7 @@ wheel
 watchdog
 flake8
 coverage
-Sphinx<8
+Sphinx
 twine
 pytest
 pytest-runner
diff --git a/setup.py b/setup.py
index dcc86978..96fb14a1 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ test_requirements = [
     'watchdog',
     'flake8',
     'coverage',
-    'Sphinx<8',
+    'Sphinx',
     'twine'
 ]
 


Test patch:
diff --git a/tests/test_filter.py b/tests/test_filter.py
index 8d8c66fc..2c3441ee 100644
--- a/tests/test_filter.py
+++ b/tests/test_filter.py
@@ -223,7 +223,7 @@ def test_crossover(impulse):
     for order in [2, 4, 6, 8]:
         for frequency in [4000, (1e2, 10e3)]:
             x = pfilt.crossover(impulse, order, frequency)
-            x_sum = np.sum(x.freq, axis=-2).flatten()
+            x_sum = np.sum(x.freq, axis=-3).flatten()
             x_ref = np.ones(x.n_bins)
             npt.assert_allclose(x_ref, np.abs(x_sum), atol=.0005)
 
@@ -254,7 +254,7 @@ def test_reconstructing_fractional_octave_bands():
     x = pf.signals.impulse(2**12)
     y, f = pfilt.reconstructing_fractional_octave_bands(x)
     assert isinstance(y, Signal)
-    assert y.cshape == (9, )
+    assert y.cshape == (9, 1)
     assert y.fft_norm == 'none'
 
     # test reconstruction (sum has a group delay of half the filter length)
@@ -279,7 +279,7 @@ def test_reconstructing_fractional_octave_bands_filter_slopes():
         # restricting rtol was not needed locally. It was added for tests to
         # pass on the testing platform
         npt.assert_allclose(
-            y.time, np.atleast_2d(reference), rtol=.01, atol=1e-10)
+            y.time[:, 0, :], np.atleast_2d(reference), rtol=.01, atol=1e-10)
 
 
 def test_reconstructing_fractional_octave_bands_warning():
diff --git a/tests/test_filter_classes.py b/tests/test_filter_classes.py
index 7a2599fc..64eae07c 100644
--- a/tests/test_filter_classes.py
+++ b/tests/test_filter_classes.py
@@ -198,7 +198,7 @@ def test_filter_iir_process_multi_dim_filt(impulse):
 
     res = filt.process(impulse)
 
-    npt.assert_allclose(res.time[:, :3], coeff[:, 0])
+    npt.assert_allclose(res.time[:, 0, :3], coeff[:, 0])
 
     impulse.time = np.vstack((impulse.time, impulse.time))
     filt = fo.FilterIIR(coeff, impulse.sampling_rate)
@@ -218,7 +218,7 @@ def test_filter_fir_process_multi_dim_filt(impulse):
 
     filt = fo.FilterFIR(coeff, impulse.sampling_rate)
     res = filt.process(impulse)
-    npt.assert_allclose(res.time[:, :3], coeff)
+    npt.assert_allclose(res.time[:, 0, :3], coeff)
 
     impulse.time = np.vstack((impulse.time, impulse.time))
     filt = fo.FilterFIR(coeff, impulse.sampling_rate)
@@ -325,7 +325,7 @@ def test_filter_sos_process_multi_dim_filt(impulse):
     filt = fo.FilterSOS(sos, impulse.sampling_rate)
     res = filt.process(impulse)
 
-    npt.assert_allclose(res.time[:, :3], coeff[:, 0])
+    npt.assert_allclose(res.time[:, 0, :3], coeff[:, 0])
 
     impulse.time = np.vstack((impulse.time, impulse.time))
     filt = fo.FilterSOS(sos, impulse.sampling_rate)
diff --git a/tests/test_fractional_octaves.py b/tests/test_fractional_octaves.py
index 645dfdc8..6dcf569c 100644
--- a/tests/test_fractional_octaves.py
+++ b/tests/test_fractional_octaves.py
@@ -135,5 +135,5 @@ def test_sum_bands_din():
 
     mask = (impulse.frequencies > 30) & (impulse.frequencies < 20e3)
 
-    assert not np.any(diff[mask] > 10**(1/10))
-    assert not np.any(diff[mask] < 10**(-1/10))
+    assert not np.any(diff[:, mask] > 10**(1/10))
+    assert not np.any(diff[:, mask] < 10**(-1/10))