def get_sleep_stats(hypno_file, output_file=None): """Compute sleep statistics from hypnogram file and export them in csv. Sleep statistics specifications: * Time in Bed (TIB) : total duration of the hypnogram. * Total Dark Time (TDT) : duration of the hypnogram from beginning to last period of sleep. * Sleep Period Time (SPT) : duration from first to last period of sleep. * Wake After Sleep Onset (WASO) : duration of wake periods within SPT * Sleep Efficiency (SE) : TST / TDT * 100 (%). * Total Sleep Time (TST) : SPT - WASO. * W, N1, N2, N3 and REM: sleep stages duration. * % (W, ... REM) : sleep stages duration expressed in percentages of TDT. * Latencies: latencies of sleep stages from the beginning of the record. (All values except SE and percentages are expressed in minutes) Parameters ---------- hypno_file : string Full path to the hypnogram file. output_file : string | None Full path to the output file. If no file is provided, sleep statictics are print out to the terminal. """ # File conversion : if output_file is not None: # Check extension ext = os.path.splitext(output_file)[1][1:].strip().lower() if ext == '': output_file = output_file + '.csv' # Load hypnogram hypno, sf_hyp = read_hypno(hypno_file) if sf_hyp < 1: mult = int(np.round(len(hypno) / sf_hyp)) hypno = oversample_hypno(hypno, mult) sf_hyp = 1 # Get sleep stats stats = sleepstats(hypno, sf_hyp=sf_hyp) stats['File'] = hypno_file print('\nSLEEP STATS\n===========') keys, val = [''] * len(stats), [''] * len(stats) # Fill table : for num, (k, v) in enumerate(stats.items()): print(k, '\t', str(v)) # Remember variables : keys[int(num)] = k val[int(num)] = str(v) if output_file is not None: write_csv(output_file, zip(keys, val)) print('===========\nCSV file saved to:', output_file)
def __init__(self, data, channels, sf, hypno, href, preload, use_mne, downsample, kwargs_mne, annotations): """Init.""" # ========================== LOAD DATA ========================== # Dialog window if data is None : if data is None: data = dialog_load( self, "Open dataset", '', "Any EEG files (*.vhdr *.edf *.gdf *.bdf *.eeg " "*.egi *.mff *.cnt *.trc *.set *.rec);;" "BrainVision (*.vhdr);;EDF (*.edf);;" "GDF (*.gdf);;BDF (*.bdf);;Elan (*.eeg);;" "EGI (*.egi);;MFF (*.mff);;CNT (*.cnt);;" "Micromed (*.trc);;EEGLab (*.set);;REC (*.rec)") upath = os.path.split(data)[0] else: upath = '' if isinstance(data, str): # file is defined # ---------- USE SLEEP or MNE ---------- # Find file extension : file, ext = get_file_ext(data) # Force to use MNE if preload is False : use_mne = True if not preload else use_mne # Get if the file has to be loaded using Sleep or MNE python : sleep_ext = ['.eeg', '.vhdr', '.edf', '.trc', '.rec'] use_mne = True if ext not in sleep_ext else use_mne if use_mne: is_mne_installed(raise_error=True) # ---------- LOAD THE FILE ---------- if use_mne: # Load using MNE functions logger.debug("Load file using MNE-python") kwargs_mne['preload'] = preload args = mne_switch(file, ext, downsample, **kwargs_mne) else: # Load using Sleep functions logger.debug("Load file using Sleep") args = sleep_switch(file, ext, downsample) # Get output arguments : (sf, downsample, dsf, data, channels, n, offset, annot) = args info = ("Data successfully loaded (%s):" "\n- Sampling-frequency : %.2fHz" "\n- Number of time points (before down-sampling): %i" "\n- Down-sampling frequency : %.2fHz" "\n- Number of time points (after down-sampling): %i" "\n- Number of channels : %i") n_channels, n_pts_after = data.shape logger.info( info % (file + ext, sf, n, downsample, n_pts_after, n_channels)) PROFILER("Data file loaded", level=1) elif isinstance(data, np.ndarray): # array of data is defined if not isinstance(sf, (int, float)): raise ValueError("When passing raw data, the sampling " "frequency parameter, sf, must either be an " "integer or a float.") file = annot = None offset = datetime.time(0, 0, 0) dsf, downsample = get_dsf(downsample, sf) n = data.shape[1] data = data[:, ::dsf] else: raise IOError("The data should either be a string which refer to " "the path of a file or an array of raw data of shape" " (n_electrodes, n_time_points).") # Keep variables : self._file = file self._annot_file = np.c_[merge_annotations(annotations, annot)] self._N = n self._dsf = dsf self._sfori = float(sf) self._toffset = offset.hour * 3600. + offset.minute * 60. + \ offset.second time = np.arange(n)[::dsf] / sf self._sf = float(downsample) if downsample is not None else float(sf) # ========================== LOAD HYPNOGRAM ========================== # Dialog window for hypnogram : if hypno is False: hypno = None elif hypno is None: hypno = dialog_load( self, "Open hypnogram", upath, "Text file (*.txt);;Elan (*.hyp);;" "CSV file (*.csv);;EDF+ file(*.edf);" ";All files (*.*)") hypno = None if hypno == '' else hypno if isinstance(hypno, np.ndarray): # array_like if len(hypno) == n: hypno = hypno[::dsf] else: raise ValueError("Then length of the hypnogram must be the " "same as raw data") if isinstance(hypno, str): # (*.hyp / *.txt / *.csv) hypno, _ = read_hypno(hypno, time=time, datafile=file) # Oversample then downsample : hypno = oversample_hypno(hypno, self._N)[::dsf] PROFILER("Hypnogram file loaded", level=1) # ========================== CHECKING ========================== # ---------- DATA ---------- # Check data shape : if data.ndim is not 2: raise ValueError("The data must be a 2D array") nchan, npts = data.shape # ---------- CHANNELS ---------- if (channels is None) or (len(channels) != nchan): warn("The number of channels must be " + str(nchan) + ". Default " "channel names will be used instead.") channels = ['chan' + str(k) for k in range(nchan)] # Clean channel names : patterns = ['eeg', 'EEG', 'ref'] chanc = [] for c in channels: # Remove informations after . : c = c.split('.')[0] c = c.split('-')[0] # Exclude patterns : for i in patterns: c = c.replace(i, '') # Remove space : c = c.replace(' ', '') c = c.strip() chanc.append(c) # ---------- STAGE ORDER ---------- # href checking : absref = ['art', 'wake', 'n1', 'n2', 'n3', 'rem'] absint = [-1, 0, 1, 2, 3, 4] if href is None: href = absref elif (href is not None) and isinstance(href, list): # Force lower case : href = [k.lower() for k in href] # Check that all stage are present : for k in absref: if k not in href: raise ValueError(k + " not found in href.") # Force capitalize : href = [k.capitalize() for k in href] href[href.index('Rem')] = 'REM' else: raise ValueError("The href parameter must be a list of string and" " must contain 'art', 'wake', 'n1', 'n2', 'n3' " "and 'rem'") # Conversion variable : absref = ['Art', 'Wake', 'N1', 'N2', 'N3', 'REM'] conv = {absint[absref.index(k)]: absint[i] for i, k in enumerate(href)} # ---------- HYPNOGRAM ---------- if hypno is None: hypno = np.zeros((npts, ), dtype=np.float32) else: n = len(hypno) # Check hypno values : if (hypno.min() < -1.) or (hypno.max() > 4) or (n != npts): warn("\nHypnogram values must be comprised between -1 and 4 " "(see Iber et al. 2007). Use:\n-1 -> Art (optional)\n 0 " "-> Wake\n 1 -> N1\n 2 -> N2\n 3 -> N4\n 4 -> REM\nEmpty " "hypnogram will be used instead") hypno = np.zeros((npts, ), dtype=np.float32) # ---------- SCALING ---------- # Assume that the inter-quartile amplitude of EEG data is ~50 uV iqr_chan = iqr(data[:, :int(data.shape[1] / 4)], axis=-1) bad_iqr = iqr_chan < 1. if np.any(bad_iqr): mult_fact = np.zeros_like(iqr_chan) iqr_chan[iqr_chan == 0.] = 1. mult_fact[bad_iqr] = np.floor(np.log10(50. / iqr_chan[bad_iqr])) data *= 10.**mult_fact[..., np.newaxis] warn("Wrong channel data amplitude. ") # ---------- CONVERSION ----------= # Convert data and hypno to be contiguous and float 32 (for vispy): self._data = vispy_array(data) self._hypno = vispy_array(hypno) self._time = vispy_array(time) self._channels = chanc self._href = href self._hconv = conv PROFILER("Check data", level=1)
def test_oversample_hypno(self): """Test function oversample_hypno.""" hyp = self._get_hypno() hyp_over = oversample_hypno(hyp, 12) to_hyp = np.array([-1, -1, 4, 4, 2, 2, 3, 3, 0, 0, 0, 0]) assert np.array_equal(hyp_over, to_hyp)
event_dict = {'W': 0, 'N1': 1, 'N2': 2, 'N3': 3, 'REM': 4, 'art': -1} # event_dict = {'W': 0, 'N1': 1, 'NREM': 2, 'REM': 4, 'art': -1} # epoch data into 30sec pieces: epochs = mne.Epochs(raw, events=dummy_events, event_id=event_dict, tmin=0, tmax=30, baseline=(0, 0), on_missing='ignore') # epochs.drop(epochs['REM'].selection) # nrem_raw = mne.io.RawArray(np.concatenate(epochs.get_data(), axis=1), raw.info) # epochs.save('406_NREM.fif') # nrem_raw.export('406_NREM.edf') # write_edf('406_NREM2.edf', nrem_raw) # rotem_edf(nrem_raw, '406_NREM3.edf') # plot spectrogram with hypnogram: data_out = raw.get_data() hypno_up = oversample_hypno(hypno, data_out.shape[1]) # spectrogram_fig, gs, ax0, ax1 = plot_spectrogram(data_out[0, :], raw.info['sfreq'], hypno_up, trimperc=10) # ax0.set(title=subject_id) stage_spect_trls = [None] * len(event_dict) stage_spect_avg = [None] * len(event_dict) stage_spect_std = [None] * len(event_dict) if separate_NREM_power_spectrum: stages_dict = {'W': 0, 'N2': 2, 'N3': 3, 'REM': 4} else: stages_dict = {'W': 0, 'N2': 2, 'REM': 4} for ind, stage in enumerate(stages_dict): if epochs[stage].__len__() > 0: # if not empty if separate_NREM_power_spectrum or stage != 'N2': psds, freqs = mne.time_frequency.psd_multitaper(epochs[stage], fmin=0.5, fmax=40, n_jobs=1)
def test_oversample_hypno(self): """Test function oversample_hypno.""" hyp = self._get_hypno() hyp_over = oversample_hypno(hyp, 12) to_hyp = np.array([-1, -1, 4, 4, 2, 2, 3, 3, 0, 0, 0, 0]) assert np.array_equal(hyp_over, to_hyp)