Repository: mne-tools/mne-python

Issue:
TF_DICS mode 'cwt_morlet' can't take list of cycle numbers for different frequency bins

#### Describe the bug
*TF_DICS beamforming with mode 'cwt_morlet' only takes deafault or single value cwt_n_cycles, although it should also accept list of float with different cycle numbers for each frequency bin*


#### Steps to reproduce
*
```
import mne
from mne import io
from mne.datasets import sample
from mne.event import make_fixed_length_events
from mne.beamformer import tf_dics
import numpy as np

print(__doc__)

# LOAD FILES FROM SAMPLE DATASET

data_path = sample.data_path()
raw_fname = data_path + '/MEG/sample/sample_audvis_raw.fif'
event_fname = data_path + '/MEG/sample/sample_audvis_raw-eve.fif'
fname_fwd = data_path + '/MEG/sample/sample_audvis-meg-eeg-oct-6-fwd.fif'
subjects_dir = data_path + '/subjects'
label_name = 'Aud-lh'
fname_label = data_path + '/MEG/sample/labels/%s.label' % label_name

# PREP EPOCHS LIKE IN TF_DICS EXAMPLE

raw = mne.io.read_raw_fif(raw_fname, preload=True)
raw.info['bads'] = ['MEG 2443']  # 1 bad MEG channel
# Pick a selection of magnetometer channels. A subset of all channels was used
# to speed up the example. For a solution based on all MEG channels use
# meg=True, selection=None and add mag=4e-12 to the reject dictionary.
left_temporal_channels = mne.read_selection('Left-temporal')
picks = mne.pick_types(raw.info, meg='mag', eeg=False, eog=False,
                       stim=False, exclude='bads',
                       selection=left_temporal_channels)
raw.pick_channels([raw.ch_names[pick] for pick in picks])
reject = dict(mag=4e-12)
# Re-normalize our empty-room projectors, which should be fine after subselection
raw.info.normalize_proj()
# Setting time windows.
tmin, tmax, tstep = -0.5, 0.75, 0.05
# Read epochs
event_id = 1
events = mne.read_events(event_fname)
epochs = mne.Epochs(raw, events, event_id, tmin, tmax,
                    baseline=None, preload=True, proj=True, reject=reject)
# Read forward operator
forward = mne.read_forward_solution(fname_fwd)
# Read label
label = mne.read_label(fname_label)


# CWT_MORLET exemplary PARAMETERS for TF-DICS beamformer to re-produce BUG/ERROR

frequencies = [[4.,5.,6.,7.,8.,9.,10.,11.,12.],[12.,13.,14.,15.,16.,17.,18.,19.,20.,21.,22.,23.,24.,25.,26.,27.,28.,29.,30.]]
win_lengths = [0.3, 0.2]
# use this line for cwt_cycles as list of float
cwt_n_cycles = [5.,7.]
# use this line to test single value
# cwt_n_cycles = 7


# TF_DICS CALCULATION

stcs = tf_dics(epochs, forward, noise_csds = None, tmin=tmin, tmax=tmax, tstep=tstep, win_lengths=win_lengths, subtract_evoked=False, mode='cwt_morlet', frequencies=frequencies, cwt_n_cycles=cwt_n_cycles,
               reg = 0.05, label=label, pick_ori='max-power', inversion='single', depth=1.0)*

```

#### Expected results
*it should work with a list of floats like the API states*


#### Actual results
*Traceback (most recent call last):
  File "bugreport_tf_dics_MWE.py", line 61, in <module>
    stcs = tf_dics(epochs, forward, noise_csds = None, tmin=tmin, tmax=tmax, tstep=tstep, win_lengths=win_lengths, subtract_evoked=False, mode='cwt_morlet', frequencies=frequencies, cwt_n_cycles=cwt_n_cycles,
  File "<decorator-gen-381>", line 24, in tf_dics
  File "/home/cora/mne-python/mne/beamformer/_dics.py", line 751, in tf_dics
    csd = csd_morlet(
  File "<decorator-gen-141>", line 22, in csd_morlet
  File "/home/cora/mne-python/mne/time_frequency/csd.py", line 935, in csd_morlet
    return csd_array_morlet(epochs.get_data(), sfreq=epochs.info['sfreq'],
  File "<decorator-gen-142>", line 22, in csd_array_morlet
  File "/home/cora/mne-python/mne/time_frequency/csd.py", line 1005, in csd_array_morlet
    wavelets = morlet(sfreq, frequencies, n_cycles)
  File "/home/cora/mne-python/mne/time_frequency/tfr.py", line 75, in morlet
    raise ValueError("n_cycles should be fixed or defined for "
ValueError: n_cycles should be fixed or defined for each frequency.*


#### Additional information
*Platform:      Linux-5.8.0-44-generic-x86_64-with-glibc2.10
Python:        3.8.8 | packaged by conda-forge | (default, Feb 20 2021, 16:22:27)  [GCC 9.3.0]
Executable:    /home/cora/anaconda3/envs/mnedev/bin/python3.8
CPU:           x86_64: 16 cores
Memory:        31.4 GB

mne:           0.23.dev0
numpy:         1.20.1 {blas=NO_ATLAS_INFO, lapack=lapack}
scipy:         1.6.0
matplotlib:    3.3.4 {backend=Qt5Agg}

sklearn:       0.24.1
numba:         0.52.0
nibabel:       3.2.1
nilearn:       0.7.0
dipy:          1.3.0
cupy:          8.5.0
pandas:        1.2.2
mayavi:        4.7.2
pyvista:       0.28.1 {pyvistaqt=0.3.0, OpenGL 4.5.0 NVIDIA 460.39 via GeForce GTX 1660 SUPER/PCIe/SSE2}
vtk:           9.0.1
PyQt5:         5.12.3*

#### Solution / Pull Request
*I've already worked out a solution that works right for both single floats and lists of floats cwt_n_cycles, and would like to make a pull request. 
The error is where the cwt_n_cycles parameter is passed to the csd_morlet() function, which can't take a list. 
The solution is to select n_cycles with an index (i.e. cwt_n_cycles[i_freq]) in the same loop that iterates over freq_bins anyways, and pass this to the csd function; and to convert single float cwt_n_cycles parameters to a list of same values with length of n_freq_bins at the beginning.*


Discussion:

Resolving patch:
diff --git a/mne/beamformer/_dics.py b/mne/beamformer/_dics.py
--- a/mne/beamformer/_dics.py
+++ b/mne/beamformer/_dics.py
@@ -679,6 +679,10 @@ def tf_dics(epochs, forward, noise_csds, tmin, tmax, tstep, win_lengths,
         raise ValueError('When using multitaper mode and specifying '
                          'multitaper transform bandwidth, one value must be '
                          'provided per frequency bin')
+    if isinstance(cwt_n_cycles, (int, float)):
+        # create a list out of single values to match n_freq_bins
+        n_cyc = cwt_n_cycles
+        cwt_n_cycles = [n_cyc] * n_freq_bins
 
     # Multiplying by 1e3 to avoid numerical issues, e.g. 0.3 // 0.05 == 5
     n_time_steps = int(((tmax - tmin) * 1e3) // (tstep * 1e3))
@@ -703,6 +707,7 @@ def tf_dics(epochs, forward, noise_csds, tmin, tmax, tstep, win_lengths,
             freq_bin = frequencies[i_freq]
             fmin = np.min(freq_bin)
             fmax = np.max(freq_bin)
+            n_cycles = cwt_n_cycles[i_freq]
         else:
             fmin, fmax = freq_bins[i_freq]
             if n_ffts is None:
@@ -753,7 +758,7 @@ def tf_dics(epochs, forward, noise_csds, tmin, tmax, tstep, win_lengths,
                 elif mode == 'cwt_morlet':
                     csd = csd_morlet(
                         epochs, frequencies=freq_bin, tmin=win_tmin,
-                        tmax=win_tmax, n_cycles=cwt_n_cycles, decim=decim,
+                        tmax=win_tmax, n_cycles=n_cycles, decim=decim,
                         verbose=False)
                 else:
                     raise ValueError('Invalid mode, choose either '


Test patch:
diff --git a/mne/beamformer/tests/test_dics.py b/mne/beamformer/tests/test_dics.py
--- a/mne/beamformer/tests/test_dics.py
+++ b/mne/beamformer/tests/test_dics.py
@@ -676,6 +676,15 @@ def test_tf_dics(_load_forward):
                 win_lengths=win_lengths, freq_bins=freq_bins,
                 mode='multitaper', mt_bandwidths=[20])
 
+    # Test if 'cwt_morlet' mode works with both fixed cycle numbers and lists
+    # of cycle numbers
+    tf_dics(epochs, fwd_surf, None, tmin, tmax, tstep,
+            win_lengths, frequencies=frequencies, mode='cwt_morlet',
+            cwt_n_cycles=7)
+    tf_dics(epochs, fwd_surf, None, tmin, tmax, tstep,
+            win_lengths, frequencies=frequencies, mode='cwt_morlet',
+            cwt_n_cycles=[5., 7.])
+
     # Test if subtracting evoked responses yields NaN's, since we only have one
     # epoch. Suppress division warnings.
     assert len(epochs) == 1, len(epochs)