Repository: mne-tools/mne-python

Issue:
Wrong dB normalization for TFR plots
#### Bug description
When using the plot() method on an AverageTFR object with the dB argument set to True, the dB normalization applied is `20*log10(data)` instead of `10*log10(data)` (which is what is specified in the documentation). 
If I'm not mistaken, `20*log10(data)` dB normalization is for amplitude data, not power data. I spotted this problem with this function, but it probably applies to other functions where the dB argument can be specified.

I suppose the problem lies here: https://github.com/mne-tools/mne-python/blob/fb80e1163ef1f89479ca7f07cb0e09e996b23cd8/mne/time_frequency/tfr.py#L2464-L2465 from the recent commit https://github.com/mne-tools/mne-python/commit/4a1bf6bb7b5b1f384ac3b1bc21fc1a660cf6f3d2



Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

I took a quick look, it appears that we need to keep track of whether data *was* complex so we can conditionally change the coef from `10` to `20` only in that case. @Martin2Constant are you up for making a PR to fix this?

Resolving patch:
diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py
--- a/mne/time_frequency/tfr.py
+++ b/mne/time_frequency/tfr.py
@@ -2411,7 +2411,9 @@ def _preproc_tfr(data, times, freqs, tmin, tmax, fmin, fmax, mode,
     data = rescale(data, times, baseline, mode, copy=copy)
 
     if np.iscomplexobj(data):
-        data = np.sqrt((data * data.conj()).real)
+        # complex amplitude → real power (for plotting); if data are
+        # real-valued they should already be power
+        data = (data * data.conj()).real
 
     # crop time
     itmin, itmax = None, None
@@ -2437,7 +2439,7 @@ def _preproc_tfr(data, times, freqs, tmin, tmax, fmin, fmax, mode,
     data = data[:, ifmin:ifmax, itmin:itmax]
 
     if dB:
-        data = 20 * np.log10(data)
+        data = 10 * np.log10(data)
 
     vmin, vmax = _setup_vmin_vmax(data, vmin, vmax)
     return data, times, freqs, vmin, vmax


Test patch:
diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py
--- a/mne/time_frequency/tests/test_tfr.py
+++ b/mne/time_frequency/tests/test_tfr.py
@@ -112,17 +112,19 @@ def test_time_frequency():
     # the other is average of the squared magnitudes (epochs PSD)
     # so values shouldn't match, but shapes should
     assert_array_equal(power.data.shape, power_evoked.data.shape)
-    pytest.raises(AssertionError, assert_allclose,
-                  power.data, power_evoked.data)
+    with pytest.raises(AssertionError, match='Not equal to tolerance'):
+        assert_allclose(power.data, power_evoked.data)
 
     # complex output
-    pytest.raises(ValueError, tfr_morlet, epochs, freqs, n_cycles,
-                  return_itc=False, average=True, output="complex")
-    pytest.raises(ValueError, tfr_morlet, epochs, freqs, n_cycles,
-                  output="complex", average=False, return_itc=True)
-    epochs_power_complex = tfr_morlet(epochs, freqs, n_cycles,
-                                      output="complex", average=False,
-                                      return_itc=False)
+    with pytest.raises(ValueError, match='must be "power" if average=True'):
+        tfr_morlet(epochs, freqs, n_cycles, return_itc=False, average=True,
+                   output='complex')
+    with pytest.raises(ValueError, match='Inter-trial coher.*average=False'):
+        tfr_morlet(epochs, freqs, n_cycles, return_itc=True, average=False,
+                   output='complex')
+    epochs_power_complex = tfr_morlet(
+        epochs, freqs, n_cycles, return_itc=False, average=False,
+        output='complex')
     epochs_amplitude_2 = abs(epochs_power_complex)
     epochs_amplitude_3 = epochs_amplitude_2.copy()
     epochs_amplitude_3.data[:] = np.inf  # test that it's actually copied
@@ -552,6 +554,29 @@ def test_init_EpochsTFR():
         del tfr
 
 
+def test_dB_computation():
+    """Test dB computation in plot methods (gh 11091)."""
+    ampl = 2.
+    data = np.full((3, 2, 3), ampl ** 2)  # already power
+    complex_data = np.full((3, 2, 3), ampl + 0j)  # ampl → power when plotting
+    times = np.array([.1, .2, .3])
+    freqs = np.array([.10, .20])
+    info = mne.create_info(['MEG 001', 'MEG 002', 'MEG 003'], 1000.,
+                           ['mag', 'mag', 'mag'])
+    kwargs = dict(times=times, freqs=freqs, nave=20, comment='test',
+                  method='crazy-tfr')
+    tfr = AverageTFR(info, data=data, **kwargs)
+    complex_tfr = AverageTFR(info, data=complex_data, **kwargs)
+    plot_kwargs = dict(dB=True, combine='mean', vmin=0, vmax=7)
+    fig1 = tfr.plot(**plot_kwargs)[0]
+    fig2 = complex_tfr.plot(**plot_kwargs)[0]
+    # since we're fixing vmin/vmax, equal colors should mean ~equal input data
+    quadmesh1 = fig1.axes[0].collections[0]
+    quadmesh2 = fig2.axes[0].collections[0]
+    if hasattr(quadmesh1, '_mapped_colors'):  # fails on compat/old
+        assert_array_equal(quadmesh1._mapped_colors, quadmesh2._mapped_colors)
+
+
 def test_plot():
     """Test TFR plotting."""
     data = np.zeros((3, 2, 3))