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
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)
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
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
def find_slowwave(data, fs): return yasa.sw_detect(data, fs)
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'])