def get_sw_templates(cam, signal, sec_before, sec_after, fs, num_ch, random=False,noise_inter=0):
    cam_time_intrp = np.arange(signal.shape[1]) * 1 / fs
    cam_intrp = np.interp(cam_time_intrp, np.arange(cam.shape[0]) / (cam.shape[0] / (signal.shape[1] / fs)), cam)
    assert(cam_intrp.shape[0] == 2400)  # just checking
    # num_ch = 1
    # # using the first channel only
    # signal = signal[0]
    templates = []
    if num_ch == 1:
        sw = sw_detect(signal, fs, hypno=None, include=(2, 3), freq_sw=(0.3, 3.5), dur_neg=(0.3, 1.5),
                       dur_pos=(0.1, 1), amp_neg=(40, 300), amp_pos=(10, 200), amp_ptp=(75, 500),
                       downsample=False, remove_outliers=False)
    else:
        sw = sw_detect_multi(signal, fs, ch_names=['EEG', 'EEG(sec)'], hypno=None, include=(2, 3), freq_sw=(0.3, 3.5),
                             dur_neg=(0.3, 1.5), dur_pos=(0.1, 1), amp_neg=(40, 300),
                             amp_pos=(10, 200), amp_ptp=(75, 500), downsample=False,
                             remove_outliers=False)

    if (sw is not None) and (len(sw) > 0):
        sw_starts = df_to_event_start(df=sw, signal=signal, fs=fs, num_ch=num_ch)
        sw_starts = replace_starts_with_random(random=random, starts=sw_starts, sec_before=sec_before,
                                               sec_after=sec_after, fs=fs, signal_len=cam_intrp.shape[0], noise_inter=noise_inter)
        templates = indices_to_templates(cam_intrp, indices=sw_starts, sec_before=sec_before, sec_after=sec_after, fs=fs)

    return templates
示例#2
0
def fcn_sw(data, sf, time, hypno):
    """Replace Visbrain built-in slow-wave detection by YASA algorithm.
    """
    # On N2 / N3 sleep only
    # Note that if you want to apply the detection on N3 sleep only, you should
    # use sw_detect(..., include=(3))
    sw = sw_detect(data, sf, hypno=hypno)
    return (sw[['Start', 'End']].values * sf).astype(int)
示例#3
0
def compute_sw_features(signal, fs, ch_names=['EEG', 'EEG(sec)']):
    if len(ch_names) == 1:
        sw = sw_detect(signal,
                       fs,
                       hypno=None,
                       include=(2, 3),
                       freq_sw=(0.3, 3.5),
                       dur_neg=(0.3, 1.5),
                       dur_pos=(0.1, 1),
                       amp_neg=(40, 300),
                       amp_pos=(10, 200),
                       amp_ptp=(75, 500),
                       downsample=False,
                       remove_outliers=False)
    else:
        sw = sw_detect_multi(signal,
                             fs,
                             ch_names=ch_names,
                             hypno=None,
                             include=(2, 3),
                             freq_sw=(0.3, 3.5),
                             dur_neg=(0.3, 1.5),
                             dur_pos=(0.1, 1),
                             amp_neg=(40, 300),
                             amp_pos=(10, 200),
                             amp_ptp=(75, 500),
                             downsample=False,
                             remove_outliers=False)

    sw_features = np.zeros(8)
    if (sw is not None) and (len(sw) > 0):
        sw_features = sw[[
            'Duration', 'ValNegPeak', 'ValPosPeak', 'PTP', 'Slope', 'Frequency'
        ]].values.mean(axis=0)
        if len(ch_names) == 2:
            num_sw_0 = np.array(
                [sw.loc[sw.Channel == 'EEG', 'Channel'].shape[0]])
            num_sw_1 = np.array(
                [sw.loc[sw.Channel == 'EEG(sec)', 'Channel'].shape[0]])
        else:
            num_sw_0 = np.array([sw.shape[0]])
            num_sw_1 = 0
        sw_features = np.append(sw_features, np.append(num_sw_0, num_sw_1))

    return sw_features
示例#4
0
def main():
    count = 600
    sn = socket.socket()
    hostn = 'localhost'
    portn = 14564
    sn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sn.bind((hostn, portn))
    print('Server Started')
    sn.listen(5)
    cn, addrn = sn.accept()
    print('Got connection from', addrn)

    mapping = {0: 'Wake', 1: 'N1', 2: 'N2', 3: 'N3', 4: 'REM'}
    # hyp = {0: 4, 1: 2, 2: 1, 3: 0, 4: 3}
    channels = {
        0: 'EEG-FPZ-CZ',
        1: 'EEG-PZ-OZ',
        2: 'EOG',
        3: 'Resp-Oro-Nasal',
        4: 'EMG',
        5: 'Temp'
    }

    npz_file = 'SleepEDF_NPZ/SC4001E0.npz'
    with np.load(npz_file) as f:
        data = f["x"]
        labels = f["y"]

    save_path = 'data/epoch_custom.npz'

    save_dict = {"x": data[count, :, :], "y": labels[count]}

    np.savez(save_path, **save_dict)

    rt = RepeatedTimer(30, func, qu)  # it auto-starts, no need of rt.start()
    while True:
        print("Epoch Number: ", count)
        save_dict = {'x': data[count, :, :], 'y': labels[count]}
        np.savez(save_path, **save_dict)
        dict_temp = qu.get()

        grads = dict_temp["grads"]
        sleepstage = dict_temp["Y_pred"]
        print(sleepstage)

        json_data = data[count, :, :]

        from datetime import datetime
        now = datetime.now()
        print("now =", now)

        root_time = strftime("%b-%d-%Y %H:%M")

        just_time = strftime("%H:%M:%S")

        json_data_eeg_fpzcz = np.transpose(np.transpose(json_data)[0][:][:])
        json_data_eeg_pzoz = np.transpose(np.transpose(json_data)[1][:][:])

        json_data_eeg_fpzcz = json_data_eeg_fpzcz.reshape(3000, )
        json_data_eeg_pzoz = json_data_eeg_pzoz.reshape(3000, )

        plt.specgram(json_data_eeg_fpzcz, Fs=100, cmap='viridis')
        plt.ylabel('Frequency [Hz]')
        plt.xlabel('Time [sec]')
        plt.savefig('../frontend/views/eeg_fpzcz_specgram.png',
                    bbox_inches='tight')

        sf = 100
        # Define window length (4 seconds)
        win = 4 * sf

        # Apply the detection using yasa.spindles_detect
        sp = yasa.spindles_detect(json_data_eeg_fpzcz, sf)

        sw = sw_detect(json_data_eeg_fpzcz,
                       sf,
                       include=(2, 3),
                       freq_sw=(0.3, 2),
                       dur_neg=(0.3, 1.5),
                       dur_pos=(0.1, 1),
                       amp_neg=(40, 300),
                       amp_pos=(10, 150),
                       amp_ptp=(75, 400),
                       remove_outliers=False,
                       coupling=False,
                       freq_sp=(12, 16))

        if sp is None:
            mask_spindles = np.zeros(len(json_data_eeg_fpzcz))
        else:
            mask_spindles = sp.get_mask()

        spindles_highlight = json_data_eeg_fpzcz * mask_spindles
        spindles_highlight[spindles_highlight == 0] = np.nan

        if sw is None:
            mask_sw = np.zeros(len(json_data_eeg_fpzcz))
        else:
            mask_sw = sw.get_mask()

        sw_highlight = json_data_eeg_fpzcz * mask_sw
        sw_highlight[sw_highlight == 0] = np.nan

        all_rows = []
        for i in range(len(json_data_eeg_fpzcz)):
            current_time = root_time + ':{}:{}0'.format(
                str(math.floor(i / 100)).zfill(2),
                str(math.floor(i % 100)).zfill(2))
            current_time = "{}".format(current_time).replace('\'', '')
            row = {}
            row["date"] = current_time
            row["EEG_FPZ_CZ"] = json_data_eeg_fpzcz[i]
            if (np.isnan(spindles_highlight[i])):
                row["EEG_FPZ_CZ_Spindle"] = None
            else:
                row["EEG_FPZ_CZ_Spindle"] = json_data_eeg_fpzcz[i]

            if (np.isnan(sw_highlight[i])):
                row["EEG_FPZ_CZ_Slow Waves"] = None
            else:
                row["EEG_FPZ_CZ_Slow Waves"] = json_data_eeg_fpzcz[i]
            all_rows.append(row)

        with open('../frontend/data/EEG-FPZ-CZ.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"').replace('None', 'null'))

        # Apply the detection using yasa.spindles_detect
        sp = yasa.spindles_detect(json_data_eeg_pzoz, sf)

        sw = sw_detect(json_data_eeg_pzoz,
                       sf,
                       include=(2, 3),
                       freq_sw=(0.3, 2),
                       dur_neg=(0.3, 1.5),
                       dur_pos=(0.1, 1),
                       amp_neg=(40, 300),
                       amp_pos=(10, 150),
                       amp_ptp=(75, 400),
                       remove_outliers=False,
                       coupling=False,
                       freq_sp=(12, 16))

        if sp is None:
            mask_spindles = np.zeros(len(json_data_eeg_pzoz))
        else:
            mask_spindles = sp.get_mask()

        spindles_highlight = json_data_eeg_pzoz * mask_spindles
        spindles_highlight[spindles_highlight == 0] = np.nan

        if sw is None:
            mask_sw = np.zeros(len(json_data_eeg_pzoz))
        else:
            mask_sw = sw.get_mask()

        sw_highlight = json_data_eeg_pzoz * mask_sw
        sw_highlight[sw_highlight == 0] = np.nan

        all_rows = []
        for i in range(len(json_data_eeg_pzoz)):
            current_time = root_time + ':{}:{}0'.format(
                str(math.floor(i / 100)).zfill(2),
                str(math.floor(i % 100)).zfill(2))
            current_time = "{}".format(current_time).replace('\'', '')
            row = {}
            row["date"] = current_time
            row["EEG_PZ_OZ"] = json_data_eeg_pzoz[i]
            if (np.isnan(spindles_highlight[i])):
                row["EEG_PZ_OZ_Spindle"] = None
            else:
                row["EEG_PZ_OZ_Spindle"] = json_data_eeg_pzoz[i]

            if (np.isnan(sw_highlight[i])):
                row["EEG_PZ_OZ_Slow Waves"] = None
            else:
                row["EEG_PZ_OZ_Slow Waves"] = json_data_eeg_pzoz[i]
            all_rows.append(row)

        with open('../frontend/data/EEG-PZ-OZ.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"').replace('None', 'null'))

        # EEG-FPZ-CZ-Grad
        all_rows = []
        for i in range(len(json_data_eeg_fpzcz)):
            current_time = root_time + ':{}:{}0'.format(
                str(math.floor(i / 100)).zfill(2),
                str(math.floor(i % 100)).zfill(2))
            current_time = "{}".format(current_time).replace('\'', '')
            row = {}
            row["date"] = current_time
            row["EEG-FPZ-CZ"] = json_data_eeg_fpzcz[i]
            if (grads[0, i] < 0.15):
                row["EEG-FPZ-CZ-Grad"] = None
            else:
                row["EEG-FPZ-CZ-Grad"] = json_data_eeg_fpzcz[i]

            all_rows.append(row)

        with open('../frontend/data/EEG-FPZ-CZ-Grad.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"').replace('None', 'null'))

        # EEG-PZ-OZ-Grad
        all_rows = []
        for i in range(len(json_data_eeg_pzoz)):
            current_time = root_time + ':{}:{}0'.format(
                str(math.floor(i / 100)).zfill(2),
                str(math.floor(i % 100)).zfill(2))
            current_time = "{}".format(current_time).replace('\'', '')
            row = {}
            row["date"] = current_time
            row["EEG-PZ-OZ"] = json_data_eeg_pzoz[i]
            if (grads[0, i] < 0.15):
                row["EEG-PZ-OZ-Grad"] = None
            else:
                row["EEG-PZ-OZ-Grad"] = json_data_eeg_pzoz[i]

            all_rows.append(row)

        with open('../frontend/data/EEG-PZ-OZ-Grad.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"').replace('None', 'null'))

        #Other channel grads
        for index in range(2, 6):
            json_data_tmp = np.transpose(np.transpose(json_data)[index][:][:])
            json_data_tmp = json_data_tmp.reshape(3000, )

            all_rows = []
            for i in range(len(json_data_tmp)):
                current_time = root_time + ':{}:{}0'.format(
                    str(math.floor(i / 100)).zfill(2),
                    str(math.floor(i % 100)).zfill(2))
                current_time = "{}".format(current_time).replace('\'', '')
                row = {}
                row["date"] = current_time
                row[channels[index]] = json_data_tmp[i]
                if (grads[0, i] < 0.15):
                    row[channels[index] + "-Grad"] = None
                else:
                    row[channels[index] + "-Grad"] = json_data_tmp[i]
                all_rows.append(row)

            with open('../frontend/data/' + channels[index] + '-Grad.json',
                      'w') as f:
                f.write(
                    str(all_rows).replace('\'', '"').replace('None', 'null'))

        #Normal non-EEG channels
        for index in range(2, 6):
            json_data_tmp = np.transpose(np.transpose(json_data)[index][:][:])
            json_data_tmp = json_data_tmp.reshape(3000, )

            all_rows = []
            for i in range(len(json_data_tmp)):
                current_time = root_time + ':{}:{}0'.format(
                    str(math.floor(i / 100)).zfill(2),
                    str(math.floor(i % 100)).zfill(2))
                current_time = "{}".format(current_time).replace('\'', '')
                row = {}
                row["date"] = current_time
                row[channels[index]] = json_data_tmp[i]
                all_rows.append(row)

            with open('../frontend/data/' + channels[index] + '.json',
                      'w') as f:
                f.write(
                    str(all_rows).replace('\'', '"').replace('None', 'null'))

        psd, freqs = psd_array_multitaper(json_data_eeg_fpzcz,
                                          sf,
                                          normalization='full',
                                          verbose=0)

        all_rows = []
        for i in range(len(freqs[:1801])):  #Upto 60Hz (Elaborate)
            row = {}
            row["Frequencies"] = freqs[i]
            row["PSD"] = psd[i]
            all_rows.append(row)

        with open('../frontend/data/PSD-FPZCZ.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"'))

        psd, freqs = psd_array_multitaper(json_data_eeg_pzoz,
                                          sf,
                                          normalization='full',
                                          verbose=0)

        all_rows = []
        for i in range(len(freqs[:1801])):
            row = {}
            row["Frequencies"] = freqs[i]
            row["PSD"] = psd[i]
            all_rows.append(row)

        with open('../frontend/data/PSD-PZOZ.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"'))

        from scipy.fftpack import rfft, rfftfreq
        SAMPLE_RATE = 100
        DURATION = 30

        # Number of samples in normalized_tone
        N = SAMPLE_RATE * DURATION

        yf = rfft(json_data_eeg_fpzcz)
        xf = rfftfreq(N, 1 / SAMPLE_RATE)

        all_rows = []
        for i in range(len(xf)):
            row = {}
            row["x"] = xf[i]
            row["y"] = np.abs(yf[i])
            all_rows.append(row)

        with open('../frontend/data/FFT-FPZCZ.json', 'w') as f:
            f.write(str(all_rows).replace('\'', '"'))

        count_json = {}
        count_json["data"] = count

        with open('../frontend/data/count.json', 'w') as f:
            f.write(str(count_json).replace('\'', '"'))

        print("Sleepstage: ", mapping[sleepstage])

        row = {}
        row["time"] = just_time
        row["stage"] = sleepstage + 1
        print(row)
        with open('../frontend/data/sleepstage.json', 'w') as f:
            f.write(str(row).replace('\'', '"'))

        # nodeserv(hyp[int(sleepstage)], cn)
        nodeserv(int(sleepstage), cn)
        count += 1
        qu.task_done()

    try:
        sleep(150)  # your long-running job goes here
    finally:
        rt.stop(
        )  # better in a try/finally block to make sure the program ends
示例#5
0
def find_slowwave(data, fs):
    return yasa.sw_detect(data, fs)
示例#6
0
def compute_features_stage(raw,
                           hypno,
                           max_freq=35,
                           spindles_params=dict(),
                           sw_params=dict()):
    """Calculate a set of features for each sleep stage from PSG data.

    Features are calculated for N2, N3, NREM (= N2 + N3) and REM sleep.

    Parameters
    ----------
    raw : :py:class:`mne.io.BaseRaw`
        An MNE Raw instance.
    hypno : array_like
        Sleep stage (hypnogram). The hypnogram must have the exact same
        number of samples as ``data``. To upsample your hypnogram,
        please refer to :py:func:`yasa.hypno_upsample_to_data`.

        .. note::
            The default hypnogram format in YASA is a 1D integer
            vector where:

            - -2 = Unscored
            - -1 = Artefact / Movement
            - 0 = Wake
            - 1 = N1 sleep
            - 2 = N2 sleep
            - 3 = N3 sleep
            - 4 = REM sleep

    max_freq : int
        Maximum frequency. This will be used to bandpass-filter the data and
        to calculate 1 Hz bins bandpower.
    kwargs_sp : dict
        Optional keywords arguments that are passed to the
        :py:func:`yasa.spindles_detect` function. We strongly recommend
        adapting the thresholds to your population (e.g. more liberal for
        older adults).
    kwargs_sw : dict
        Optional keywords arguments that are passed to the
        :py:func:`yasa.sw_detect` function. We strongly recommend
        adapting the thresholds to your population (e.g. more liberal for
        older adults).
    """
    # #########################################################################
    # 2) PREPROCESSING
    # #########################################################################

    # Safety checks
    assert isinstance(max_freq, int), "`max_freq` must be int."
    assert isinstance(raw, mne.io.BaseRaw), "`raw` must be a MNE Raw object."
    assert isinstance(spindles_params, dict)
    assert isinstance(sw_params, dict)

    # Define 1 Hz bins frequency bands for bandpower
    # Similar to [(0.5, 1, "0.5-1"), (1, 2, "1-2"), ..., (34, 35, "34-35")]
    bands = []
    freqs = [0.5] + list(range(1, max_freq + 1))
    for i, b in enumerate(freqs[:-1]):
        bands.append(tuple((b, freqs[i + 1], "%s-%s" % (b, freqs[i + 1]))))
    # Append traditional bands
    bands_classic = [(0.5, 1, 'slowdelta'), (1, 4, 'fastdelta'),
                     (0.5, 4, 'delta'), (4, 8, 'theta'), (8, 12, 'alpha'),
                     (12, 16, 'sigma'), (16, 30, 'beta'),
                     (30, max_freq, 'gamma')]
    bands = bands_classic + bands

    # Find min and maximum frequencies. These will be used for bandpass-filter
    # and 1/f adjustement of bandpower. l_freq = 0.5 / h_freq = 35 Hz.
    all_freqs_sorted = np.sort(
        np.unique([b[0] for b in bands] + [b[1] for b in bands]))
    l_freq = all_freqs_sorted[0]
    h_freq = all_freqs_sorted[-1]

    # Mapping dictionnary integer to string for sleep stages (2 --> N2)
    stage_mapping = {
        -2: 'Unscored',
        -1: 'Artefact',
        0: 'Wake',
        1: 'N1',
        2: 'N2',
        3: 'N3',
        4: 'REM',
        6: 'NREM',
        7: 'WN'  # Whole night = N2 + N3 + REM
    }

    # Hypnogram check + calculate NREM hypnogram
    hypno = np.asarray(hypno, dtype=int)
    assert hypno.ndim == 1, 'Hypno must be one dimensional.'
    unique_hypno = np.unique(hypno)
    logger.info('Number of unique values in hypno = %i', unique_hypno.size)

    # IMPORTANT: NREM is defined as N2 + N3, excluding N1 sleep.
    hypno_NREM = pd.Series(hypno).replace({2: 6, 3: 6}).to_numpy()
    minutes_of_NREM = (hypno_NREM == 6).sum() / (60 * raw.info['sfreq'])

    # WN = Whole night = N2 + N3 + REM (excluding N1)
    hypno_WN = pd.Series(hypno).replace({2: 7, 3: 7, 4: 7}).to_numpy()
    # minutes_of_WN = (hypno_WN == 7).sum() / (60 * raw.info['sfreq'])

    # Keep only EEG channels
    raw_eeg = raw.pick_types(eeg=True)

    # Remove suffix from channels: C4-M1 --> C4
    chan_nosuffix = [c.split('-')[0] for c in raw_eeg.ch_names]
    raw_eeg.rename_channels(dict(zip(raw_eeg.ch_names, chan_nosuffix)))
    # Rename P7/T5 --> P7
    chan_noslash = [c.split('/')[0] for c in raw_eeg.ch_names]
    raw_eeg.rename_channels(dict(zip(raw_eeg.ch_names, chan_noslash)))
    chan = raw_eeg.ch_names

    # Resample to 100 Hz and bandpass-filter
    raw_eeg.resample(100, verbose=False)
    raw_eeg.filter(l_freq, h_freq, verbose=False)

    # Extract data and sf
    data = raw_eeg.get_data() * 1e6  # Scale from Volts (MNE default) to uV
    sf = raw_eeg.info['sfreq']
    assert data.ndim == 2, 'data must be 2D (chan, times).'
    assert hypno.size == data.shape[1], 'Hypno must have same size as data.'

    # #########################################################################
    # 2) SPECTRAL POWER
    # #########################################################################

    print("  ..calculating spectral powers")

    # 2.1) 1Hz bins, N2 / N3 / REM
    # win_sec = 4 sec = 0.25 Hz freq resolution
    df_bp = yasa.bandpower(raw_eeg,
                           hypno=hypno,
                           bands=bands,
                           win_sec=4,
                           include=(2, 3, 4))
    # Same for NREM / WN
    df_bp_NREM = yasa.bandpower(raw_eeg,
                                hypno=hypno_NREM,
                                bands=bands,
                                include=6)
    df_bp_WN = yasa.bandpower(raw_eeg, hypno=hypno_WN, bands=bands, include=7)
    df_bp = df_bp.append(df_bp_NREM).append(df_bp_WN)
    df_bp.drop(columns=['TotalAbsPow', 'FreqRes', 'Relative'], inplace=True)
    df_bp = df_bp.add_prefix('bp_').reset_index()
    # Replace 2 --> N2
    df_bp['Stage'] = df_bp['Stage'].map(stage_mapping)
    # Assert that there are no negative values (see below issue on 1/f)
    assert not (df_bp._get_numeric_data() < 0).any().any()
    df_bp.columns = df_bp.columns.str.lower()

    # 2.2) Same but after adjusting for 1/F (VERY SLOW!)
    # This is based on the IRASA method described in Wen & Liu 2016.
    df_bp_1f = []

    for stage in [2, 3, 4, 6, 7]:
        if stage == 6:
            # Use hypno_NREM
            data_stage = data[:, hypno_NREM == stage]
        elif stage == 7:
            # Use hypno_WN
            data_stage = data[:, hypno_WN == stage]
        else:
            data_stage = data[:, hypno == stage]
        # Skip if stage is not present in data
        if data_stage.shape[-1] == 0:
            continue
        # Calculate aperiodic / oscillatory PSD + slope
        freqs, _, psd_osc, fit_params = yasa.irasa(data_stage,
                                                   sf,
                                                   ch_names=chan,
                                                   band=(l_freq, h_freq),
                                                   win_sec=4)
        # Make sure that we don't have any negative values in PSD
        # See https://github.com/raphaelvallat/yasa/issues/29
        psd_osc = psd_osc - psd_osc.min(axis=-1, keepdims=True)
        # Calculate bandpower
        bp = yasa.bandpower_from_psd(psd_osc,
                                     freqs,
                                     ch_names=chan,
                                     bands=bands)
        # Add 1/f slope to dataframe and sleep stage
        bp['1f_slope'] = np.abs(fit_params['Slope'].to_numpy())
        bp.insert(loc=0, column="Stage", value=stage_mapping[stage])
        df_bp_1f.append(bp)

    # Convert to a dataframe
    df_bp_1f = pd.concat(df_bp_1f)
    # Remove the TotalAbsPower column, incorrect because of negative values
    df_bp_1f.drop(columns=['TotalAbsPow', 'FreqRes', 'Relative'], inplace=True)
    df_bp_1f.columns = [
        c if c in ['Stage', 'Chan', '1f_slope'] else 'bp_adj_' + c
        for c in df_bp_1f.columns
    ]
    assert not (df_bp_1f._get_numeric_data() < 0).any().any()
    df_bp_1f.columns = df_bp_1f.columns.str.lower()

    # #########################################################################
    # 3) SPINDLES DETECTION // WORK IN PROGRESS
    # #########################################################################

    # print("  ..detecting sleep spindles")

    # spindles_params.update(include=(2, 3))

    # # Detect spindles in N2 and N3
    # # Thresholds have to be tuned with visual scoring of a subset of data
    # # https://raphaelvallat.com/yasa/build/html/generated/yasa.spindles_detect.html
    # sp = yasa.spindles_detect(raw_eeg, hypno=hypno, **spindles_params)

    # df_sp = sp.summary(grp_chan=True, grp_stage=True).reset_index()
    # df_sp['Stage'] = df_sp['Stage'].map(stage_mapping)

    # # Aggregate using the mean (adding NREM = N2 + N3)
    # df_sp = sp.summary(grp_chan=True, grp_stage=True)
    # df_sp_NREM = sp.summary(grp_chan=True).reset_index()
    # df_sp_NREM['Stage'] = 6
    # df_sp_NREM.set_index(['Stage', 'Channel'], inplace=True)
    # density_NREM = df_sp_NREM['Count'] / minutes_of_NREM
    # df_sp_NREM.insert(loc=1, column='Density',
    #                   value=density_NREM.to_numpy())

    # df_sp = df_sp.append(df_sp_NREM)
    # df_sp.columns = ['sp_' + c if c in ['Count', 'Density'] else
    #                  'sp_mean_' + c for c in df_sp.columns]

    # # Prepare to export
    # df_sp.reset_index(inplace=True)
    # df_sp['Stage'] = df_sp['Stage'].map(stage_mapping)
    # df_sp.columns = df_sp.columns.str.lower()
    # df_sp.rename(columns={'channel': 'chan'}, inplace=True)

    # #########################################################################
    # 4) SLOW-WAVES DETECTION & SW-Sigma COUPLING
    # #########################################################################

    print("  ..detecting slow-waves")

    # Make sure we calculate coupling
    sw_params.update(coupling=True)

    # Detect slow-waves
    # Option 1: Using absolute thresholds
    # IMPORTANT: THRESHOLDS MUST BE ADJUSTED ACCORDING TO AGE!
    sw = yasa.sw_detect(raw_eeg, hypno=hypno, **sw_params)

    # Aggregate using the mean per channel x stage
    df_sw = sw.summary(grp_chan=True, grp_stage=True)
    # Add NREM
    df_sw_NREM = sw.summary(grp_chan=True).reset_index()
    df_sw_NREM['Stage'] = 6
    df_sw_NREM.set_index(['Stage', 'Channel'], inplace=True)
    density_NREM = df_sw_NREM['Count'] / minutes_of_NREM
    df_sw_NREM.insert(loc=1, column='Density', value=density_NREM.to_numpy())
    df_sw = df_sw.append(df_sw_NREM)[[
        'Count', 'Density', 'Duration', 'PTP', 'Frequency', 'ndPAC'
    ]]
    df_sw.columns = [
        'sw_' + c if c in ['Count', 'Density'] else 'sw_mean_' + c
        for c in df_sw.columns
    ]

    # Aggregate using the coefficient of variation
    # The CV is a normalized (unitless) standard deviation. Lower
    # values mean that slow-waves are more similar to each other.
    # We keep only spefific columns of interest. Not duration because it
    # is highly correlated with frequency (r=0.99).
    df_sw_cv = sw.summary(grp_chan=True,
                          grp_stage=True,
                          aggfunc=sp_stats.variation)[[
                              'PTP', 'Frequency', 'ndPAC'
                          ]]

    # Add NREM
    df_sw_cv_NREM = sw.summary(grp_chan=True,
                               grp_stage=False,
                               aggfunc=sp_stats.variation)[[
                                   'PTP', 'Frequency', 'ndPAC'
                               ]].reset_index()
    df_sw_cv_NREM['Stage'] = 6
    df_sw_cv_NREM.set_index(['Stage', 'Channel'], inplace=True)
    df_sw_cv = df_sw_cv.append(df_sw_cv_NREM)
    df_sw_cv.columns = ['sw_cv_' + c for c in df_sw_cv.columns]

    # Combine the mean and CV into a single dataframe
    df_sw = df_sw.join(df_sw_cv).reset_index()
    df_sw['Stage'] = df_sw['Stage'].map(stage_mapping)
    df_sw.columns = df_sw.columns.str.lower()
    df_sw.rename(columns={'channel': 'chan'}, inplace=True)

    # #########################################################################
    # 5) ENTROPY & FRACTAL DIMENSION
    # #########################################################################

    print("  ..calculating entropy measures")

    # Filter data in the delta band and calculate envelope for CVE
    data_delta = mne.filter.filter_data(data,
                                        sfreq=sf,
                                        l_freq=0.5,
                                        h_freq=4,
                                        l_trans_bandwidth=0.2,
                                        h_trans_bandwidth=0.2,
                                        verbose=False)
    env_delta = np.abs(sp_sig.hilbert(data_delta))

    # Initialize dataframe
    idx_ent = pd.MultiIndex.from_product([[2, 3, 4, 6, 7], chan],
                                         names=['stage', 'chan'])
    df_ent = pd.DataFrame(index=idx_ent)

    for stage in [2, 3, 4, 6, 7]:
        if stage == 6:
            # Use hypno_NREM
            data_stage = data[:, hypno_NREM == stage]
            env_stage_delta = env_delta[:, hypno_NREM == stage]
        elif stage == 7:
            # Use hypno_WN
            data_stage = data[:, hypno_WN == stage]
            env_stage_delta = env_delta[:, hypno_WN == stage]
        else:
            data_stage = data[:, hypno == stage]
            env_stage_delta = env_delta[:, hypno == stage]
        # Skip if stage is not present in data
        if data_stage.shape[-1] == 0:
            continue

        # Entropy and fractal dimension (FD)
        # See review here: https://pubmed.ncbi.nlm.nih.gov/33286013/
        # These are calculated on the broadband signal.
        # - DFA not implemented because it is dependent on data length.
        # - Spectral entropy not implemented because the data is not
        #   continuous (segmented by stage).
        # - Sample / app entropy not implemented because it is too slow to
        #   calculate.
        from numpy import apply_along_axis as aal
        df_ent.loc[stage, 'ent_svd'] = aal(ant.svd_entropy,
                                           axis=1,
                                           arr=data_stage,
                                           normalize=True)
        df_ent.loc[stage, 'ent_perm'] = aal(ant.perm_entropy,
                                            axis=1,
                                            arr=data_stage,
                                            normalize=True)
        df_ent.loc[stage, 'ent_higuchi'] = aal(ant.higuchi_fd,
                                               axis=1,
                                               arr=data_stage)

        # We also add the coefficient of variation of the delta envelope
        # (CVE), a measure of "slow-wave stability".
        # See Diaz et al 2018, NeuroImage / Park et al 2021, Sci. Rep.
        # Lower values = more stable slow-waves (= more sinusoidal)
        denom = np.sqrt(4 / np.pi - 1)  # approx 0.5227
        cve = sp_stats.variation(env_stage_delta, axis=1) / denom
        df_ent.loc[stage, 'ent_cve_delta'] = cve

    df_ent = df_ent.dropna(how="all").reset_index()
    df_ent['stage'] = df_ent['stage'].map(stage_mapping)

    # #########################################################################
    # 5) MERGE ALL DATAFRAMES
    # #########################################################################

    df = (
        df_bp.merge(df_bp_1f, how='outer')
        # .merge(df_sp, how='outer')
        .merge(df_sw, how='outer').merge(df_ent, how='outer'))

    return df.set_index(['stage', 'chan'])