Repository: pyfar/pyfar
Pull request #671 (2024-08-22 05:14:53)

Plot complex spectogram
### Which issue(s) are closed by this pull request?

Closes #

### Changes proposed in this pull request:

-
-
-



Patch:
diff --git a/pyfar/plot/_two_d.py b/pyfar/plot/_two_d.py
index 92ce8725..81bbef6c 100644
--- a/pyfar/plot/_two_d.py
+++ b/pyfar/plot/_two_d.py
@@ -324,7 +324,7 @@ def _freq_group_delay_2d(
 def _spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
                  freq_scale='linear', unit="s", window='hann',
                  window_length=1024, window_overlap_fct=0.5,
-                 colorbar=True, ax=None, **kwargs):
+                 colorbar=True, ax=None, side='right', **kwargs):
     """Plot the magnitude spectrum versus time.
 
     See pyfar.line.spectogram for more information.
@@ -363,6 +363,11 @@ def _spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
     frequencies = frequencies.squeeze()
     spectrogram = spectrogram.squeeze()
 
+    # adapt data according to side
+    spectrogram, frequencies, ylabel = \
+        _utils._assert_and_match_spectrogram_to_side(spectrogram, frequencies,
+                                                     signal, side)
+
     # get magnitude data in dB
     if dB:
         if log_prefix is None:
@@ -386,7 +391,7 @@ def _spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
     qm = ax[0].pcolormesh(times, frequencies, spectrogram, **kwargs)
 
     # Adjust axes:
-    ax[0].set_ylabel('Frequency in Hz')
+    ax[0].set_ylabel(ylabel)
     ax[0].set_xlabel(f'Time in {unit}')
     ax[0].set_xlim((times[0], times[-1]))
     ax[0].set_ylim((max(20, frequencies[1]), signal.sampling_rate/2))
diff --git a/pyfar/plot/_utils.py b/pyfar/plot/_utils.py
index 467ffcec..fbcde8b1 100644
--- a/pyfar/plot/_utils.py
+++ b/pyfar/plot/_utils.py
@@ -457,6 +457,34 @@ def _assert_and_match_data_to_side(data, signal, side):
     return data, frequencies, xlabel
 
 
+def _assert_and_match_spectrogram_to_side(spectrogram, frequencies, signal,
+                                          side):
+    """Adjust data and frequency vector for plotting as specified by side."""
+
+    if side == 'left':
+        mask = frequencies <= 0
+    elif side == 'right':
+        mask = frequencies >= 0
+    else:
+        raise ValueError('Invalid `side` parameter, pass either `left` or '
+                         '`right`.')
+
+    # get corresponding data
+    frequencies = frequencies[mask]
+    spectrogram = spectrogram[..., mask, :]
+
+    if side == 'left':
+        frequencies = np.flipud(np.abs(frequencies))
+        spectrogram = spectrogram[::-1, ...]
+
+    if (type(signal) is not FrequencyData) and signal.complex:
+        xlabel = f"Frequency in Hz ({side})"
+    else:
+        xlabel = "Frequency in Hz"
+
+    return spectrogram, frequencies, xlabel
+
+
 def _assert_and_match_data_to_mode(data, signal, mode):
     """Adjust data and y-label for plotting according to specified mode."""
 
diff --git a/pyfar/plot/two_d.py b/pyfar/plot/two_d.py
index 5424c144..91f09599 100644
--- a/pyfar/plot/two_d.py
+++ b/pyfar/plot/two_d.py
@@ -1048,7 +1048,8 @@ def freq_group_delay_2d(signal, dB=True, log_prefix=None, log_reference=1,
 def spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
                 freq_scale='linear', unit='s', window='hann',
                 window_length=1024, window_overlap_fct=0.5,
-                colorbar=True, ax=None, style='light', **kwargs):
+                colorbar=True, ax=None, style='light', side='right',
+                **kwargs):
     """Plot blocks of the magnitude spectrum versus time.
 
     Parameters
@@ -1121,6 +1122,11 @@ def spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
         ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary
         ``style = {}`` to use the currently active plotstyle. The default is
         ``light``.
+    side : str, optional
+        ``'right'`` to plot the right-sided spectrum containing the positive
+        frequencies, or ``'left'``to plot the left-sided spectrum containing
+        the negative frequencies (only possible for complex Signals). The
+        default is ``'right'``.
     **kwargs
         Keyword arguments that are passed to
         :py:func:`matplotlib.pyplot.pcolormesh` or
@@ -1161,7 +1167,8 @@ def spectrogram(signal, dB=True, log_prefix=None, log_reference=1,
     with context(style):
         ax, qm, cb = _two_d._spectrogram(
             signal.flatten(), dB, log_prefix, log_reference, freq_scale, unit,
-            window, window_length, window_overlap_fct, colorbar, ax, **kwargs)
+            window, window_length, window_overlap_fct, colorbar, ax, side,
+            **kwargs)
 
     # manage interaction
     plot_parameter = ia.PlotParameter(


Test patch:
diff --git a/tests/test_plot.py b/tests/test_plot.py
index 8b86bdfd..7f4d414b 100644
--- a/tests/test_plot.py
+++ b/tests/test_plot.py
@@ -303,6 +303,26 @@ def test_spectrogram():
                      file_type, compare_output)
 
 
+@pytest.mark.parametrize('side', ['left', 'right'])
+def test_spectrogram_complex(side):
+    """Test spectrogram with default parameters"""
+    function = plot.spectrogram
+
+    print(f"Testing: {function.__name__}")
+
+    filename = function.__name__ + f'_complex_{side}_default'
+    create_figure()
+
+    sweep = pf.signals.exponential_sweep_time(2**16, [100, 10e3])
+
+    sweep.fft_norm = "none"
+    sweep.complex = True
+
+    function(sweep, side=side)
+    save_and_compare(create_baseline, baseline_path, output_path, filename,
+                     file_type, compare_output)
+
+
 @pytest.mark.parametrize('function', [
     (plot.time_2d), (plot.freq_2d), (plot.phase_2d), (plot.group_delay_2d),
     (plot.time_freq_2d), (plot.freq_phase_2d), (plot.freq_group_delay_2d)])
diff --git a/tests/test_plot__utils.py b/tests/test_plot__utils.py
index 99a8bb2e..ffbc1a0c 100644
--- a/tests/test_plot__utils.py
+++ b/tests/test_plot__utils.py
@@ -233,3 +233,20 @@ def test_assert_and_match_data_to_mode(mode, ylabel):
         npt.assert_allclose(data,
                             np.abs(signal.time), atol=1e-15)
     assert _ylabel == ylabel
+
+
+@pytest.mark.parametrize('side', ['left', 'right'])
+def test_assert_and_match_spectrogram_to_side(side):
+
+    signal = pf.Signal(data=[1, 4, 5, 6, 7], sampling_rate=48000,
+                       is_complex=True)
+
+    frequencies, _, spec = pf.dsp.spectrogram(signal, window_length=4)
+
+    spec, frequencies, xlabel = \
+        plot._utils._assert_and_match_spectrogram_to_side(
+            np.squeeze(spec, axis=0), frequencies, signal, side)
+
+    assert not np.any(frequencies < 0.0)
+    assert spec.shape[0] == frequencies.shape[0]
+    assert xlabel == f'Frequency in Hz ({side})'