misleep 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- misleep/__init__.py +111 -0
- misleep/__main__.py +16 -0
- misleep/analysis/__init__.py +23 -0
- misleep/analysis/auto_stage.py +158 -0
- misleep/analysis/detection.py +198 -0
- misleep/analysis/features.py +131 -0
- misleep/analysis/models/P30_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/P30_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/__init__.py +0 -0
- misleep/analysis/models/ado_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/ado_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/transformer/__init__.py +46 -0
- misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
- misleep/analysis/transformer/checkpoints/__init__.py +0 -0
- misleep/analysis/transformer/configs.py +52 -0
- misleep/analysis/transformer/data.py +362 -0
- misleep/analysis/transformer/inference.py +328 -0
- misleep/analysis/transformer/models.py +220 -0
- misleep/analysis/transformer/preprocessing.py +105 -0
- misleep/config/__init__.py +105 -0
- misleep/config/default_config.ini +38 -0
- misleep/data/__init__.py +13 -0
- misleep/data/annotation.py +114 -0
- misleep/data/midata.py +387 -0
- misleep/gui/__init__.py +23 -0
- misleep/gui/app.py +183 -0
- misleep/gui/config_dialog.py +429 -0
- misleep/gui/dialogs.py +1107 -0
- misleep/gui/event_filters.py +26 -0
- misleep/gui/main_window.py +2699 -0
- misleep/gui/qt_utils.py +277 -0
- misleep/gui/resources/__init__.py +9 -0
- misleep/gui/resources/entire_logo.png +0 -0
- misleep/gui/resources/logo.png +0 -0
- misleep/gui/resources/misleep.ico +0 -0
- misleep/gui/resources/misleep.qrc +7 -0
- misleep/gui/resources/misleep_rc.py +1324 -0
- misleep/gui/spec_window.py +187 -0
- misleep/gui/style.py +994 -0
- misleep/gui/uis/SWA_detect_dialog.ui +181 -0
- misleep/gui/uis/SWA_detect_dialog_ui.py +176 -0
- misleep/gui/uis/__init__.py +6 -0
- misleep/gui/uis/about.ui +88 -0
- misleep/gui/uis/about_ui.py +80 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +77 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +90 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog.ui +124 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +124 -0
- misleep/gui/uis/horizontal_line_dialog.ui +166 -0
- misleep/gui/uis/horizontal_line_dialog_ui.py +157 -0
- misleep/gui/uis/label_dialog.ui +65 -0
- misleep/gui/uis/label_dialog_ui.py +72 -0
- misleep/gui/uis/main_window.ui +871 -0
- misleep/gui/uis/main_window_ui.py +616 -0
- misleep/gui/uis/save_data_dialog.ui +95 -0
- misleep/gui/uis/save_data_dialog_ui.py +98 -0
- misleep/gui/uis/spec_window.ui +109 -0
- misleep/gui/uis/spec_window_ui.py +103 -0
- misleep/gui/uis/spindle_detect_dialog.ui +202 -0
- misleep/gui/uis/spindle_detect_dialog_ui.py +194 -0
- misleep/gui/uis/state_spectral_dialog.ui +240 -0
- misleep/gui/uis/state_spectral_dialog_ui.py +204 -0
- misleep/gui/uis/transfer_result_dialog.ui +104 -0
- misleep/gui/uis/transfer_result_dialog_ui.py +94 -0
- misleep/gui/workers.py +69 -0
- misleep/io/__init__.py +68 -0
- misleep/io/annotation.py +367 -0
- misleep/io/array.py +209 -0
- misleep/io/base.py +174 -0
- misleep/io/edf.py +109 -0
- misleep/io/mat.py +189 -0
- misleep/logger.py +79 -0
- misleep/preprocessing/__init__.py +23 -0
- misleep/preprocessing/artifacts.py +76 -0
- misleep/preprocessing/filtering.py +94 -0
- misleep/preprocessing/segment.py +59 -0
- misleep/preprocessing/spectral.py +169 -0
- misleep/utils/__init__.py +45 -0
- misleep/utils/annotation.py +130 -0
- misleep/utils/entropy.py +135 -0
- misleep/utils/misc.py +69 -0
- misleep/utils/time_utils.py +61 -0
- misleep/viz/__init__.py +12 -0
- misleep/viz/hypnogram.py +48 -0
- misleep/viz/signals.py +51 -0
- misleep/viz/spectral.py +98 -0
- misleep-0.3.0.dist-info/METADATA +249 -0
- misleep-0.3.0.dist-info/RECORD +94 -0
- misleep-0.3.0.dist-info/WHEEL +5 -0
- misleep-0.3.0.dist-info/entry_points.txt +15 -0
- misleep-0.3.0.dist-info/licenses/LICENSE +29 -0
- misleep-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Signal filtering.
|
|
3
|
+
|
|
4
|
+
Currently provides a Butterworth zero-phase filter through
|
|
5
|
+
:func:`signal_filter` and mains (power-line) noise removal through
|
|
6
|
+
:func:`filter_power_line_noise`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy import signal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def signal_filter(data, sf=256.0, btype="lowpass", low=0.5, high=30.0):
|
|
14
|
+
"""Filter a signal with a zero-phase Butterworth filter.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
data : ndarray
|
|
19
|
+
1-D array, the signal to filter.
|
|
20
|
+
sf : float
|
|
21
|
+
Sampling frequency of the signal. Default is 256.
|
|
22
|
+
btype : {'lowpass', 'highpass', 'bandpass', 'bandstop'}, optional
|
|
23
|
+
The type of filter. Default is ``'lowpass'``.
|
|
24
|
+
low : float
|
|
25
|
+
Lower cutoff frequency (Hz), used by ``'highpass'``, ``'bandpass'``
|
|
26
|
+
and ``'bandstop'``.
|
|
27
|
+
high : float
|
|
28
|
+
Higher cutoff frequency (Hz), used by ``'lowpass'``, ``'bandpass'``
|
|
29
|
+
and ``'bandstop'``.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
filtered_data : ndarray
|
|
34
|
+
The filtered signal.
|
|
35
|
+
fname : str
|
|
36
|
+
A short name describing the filter, e.g. ``'bandpass_0.5_30'``.
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(sf, (int, float)):
|
|
39
|
+
raise TypeError(f"Sample frequency should be a float, got {type(sf)}")
|
|
40
|
+
if not isinstance(low, (int, float)):
|
|
41
|
+
raise TypeError(f"Low threshold should be a float, got {type(low)}")
|
|
42
|
+
if not isinstance(high, (int, float)):
|
|
43
|
+
raise TypeError(f"High threshold should be a float, got {type(high)}")
|
|
44
|
+
|
|
45
|
+
if btype == "lowpass":
|
|
46
|
+
fnorm = high / (0.5 * sf)
|
|
47
|
+
fname = f"{btype}_{high}"
|
|
48
|
+
elif btype == "highpass":
|
|
49
|
+
fnorm = low / (0.5 * sf)
|
|
50
|
+
fname = f"{btype}_{low}"
|
|
51
|
+
elif btype in ("bandpass", "bandstop"):
|
|
52
|
+
fnorm = np.divide([low, high], 0.5 * sf)
|
|
53
|
+
fname = f"{btype}_{low}_{high}"
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"'{btype}' is an invalid type for filter, you can only choose "
|
|
57
|
+
f"'lowpass', 'highpass', 'bandpass' or 'bandstop'")
|
|
58
|
+
|
|
59
|
+
b, a = signal.iirfilter(N=3, Wn=fnorm, btype=btype, analog=False,
|
|
60
|
+
output="ba", ftype="butter", fs=None)
|
|
61
|
+
filtered_data = signal.filtfilt(b=b, a=a, x=data)
|
|
62
|
+
|
|
63
|
+
return filtered_data, fname
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def filter_power_line_noise(data, sf, noise_band="50-100-150"):
|
|
67
|
+
"""Remove mains (power-line) noise with band-stop filters.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
data : ndarray
|
|
72
|
+
Signal to filter.
|
|
73
|
+
sf : float
|
|
74
|
+
Sampling frequency.
|
|
75
|
+
noise_band : {'50-100-150', '60-120-180'}, optional
|
|
76
|
+
Mains frequency harmonics to remove. ``'60-120-180'`` is not yet
|
|
77
|
+
implemented.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
ndarray
|
|
82
|
+
The filtered signal.
|
|
83
|
+
"""
|
|
84
|
+
filter_band = []
|
|
85
|
+
if noise_band == "50-100-150" and sf > 306:
|
|
86
|
+
filter_band = [[47, 53], [97, 103], [147, 153]]
|
|
87
|
+
elif noise_band == "50-100-150" and sf > 206:
|
|
88
|
+
filter_band = [[47, 53], [97, 103]]
|
|
89
|
+
elif sf > 106:
|
|
90
|
+
filter_band = [[47, 53]]
|
|
91
|
+
for each in filter_band:
|
|
92
|
+
data, _ = signal_filter(data, sf, btype="bandstop", low=each[0], high=each[1])
|
|
93
|
+
|
|
94
|
+
return data
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Segmentation of signals by sleep state.
|
|
3
|
+
|
|
4
|
+
The :func:`crop_state_data` helper splits a recording into per-state
|
|
5
|
+
sub-recordings (NREM / REM / Wake / Init) based on the annotation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from misleep.data import MiData
|
|
13
|
+
from misleep.utils.annotation import lst2group
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def crop_state_data(midata, mianno):
|
|
17
|
+
"""Split the data into per-state sub-recordings.
|
|
18
|
+
|
|
19
|
+
The sleep-state sequence is grouped into consecutive runs; the samples
|
|
20
|
+
of every run of the same state are concatenated, and a new
|
|
21
|
+
:class:`MiData` is returned per state.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
midata : MiData
|
|
26
|
+
The recording to split.
|
|
27
|
+
mianno : MiAnnotation
|
|
28
|
+
The annotation defining the states.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
(NREM_data, REM_data, Wake_data, Init_data) : tuple of MiData
|
|
33
|
+
One data container per state, with ``describe`` set accordingly.
|
|
34
|
+
States not present in the annotation yield empty signals.
|
|
35
|
+
"""
|
|
36
|
+
sleep_state = deepcopy(mianno.sleep_state)
|
|
37
|
+
sleep_state = lst2group([[idx, each] for idx, each in enumerate(sleep_state)])
|
|
38
|
+
signals = deepcopy(midata.signals)
|
|
39
|
+
|
|
40
|
+
state_signals = {1: [], 2: [], 3: [], 4: []}
|
|
41
|
+
for idx, signal in enumerate(signals):
|
|
42
|
+
sf = midata.sf[idx]
|
|
43
|
+
for state in state_signals:
|
|
44
|
+
state_data = [signal[int(each[0] * sf): int(each[1] * sf)]
|
|
45
|
+
for each in sleep_state if each[2] == state]
|
|
46
|
+
state_signals[state].append(
|
|
47
|
+
np.array([element for sublist in state_data for element in sublist]))
|
|
48
|
+
|
|
49
|
+
names = {1: "NREM", 2: "REM", 3: "Wake", 4: "Init"}
|
|
50
|
+
results = []
|
|
51
|
+
for state in (1, 2, 3, 4):
|
|
52
|
+
results.append(MiData(
|
|
53
|
+
signals=state_signals[state],
|
|
54
|
+
channels=midata.channels,
|
|
55
|
+
sf=midata.sf,
|
|
56
|
+
time=midata.time,
|
|
57
|
+
describe=f"{names[state]} cropped data"))
|
|
58
|
+
|
|
59
|
+
return tuple(results)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Spectral analysis: Welch power spectrum, spectrogram and band power."""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.integrate import simpson
|
|
6
|
+
from scipy.ndimage import gaussian_filter1d
|
|
7
|
+
from scipy.signal import stft, welch
|
|
8
|
+
|
|
9
|
+
from misleep.preprocessing.filtering import signal_filter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def spectrum(signal, sf, band=None, relative=True, win_sec=1, nfft=None, gaussian_sigma=None):
|
|
13
|
+
"""Calculate the (Welch) power spectrum of a signal.
|
|
14
|
+
|
|
15
|
+
The signal is band-pass filtered to ``band`` first, then the power
|
|
16
|
+
spectral density is estimated with :func:`scipy.signal.welch`.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
signal : ndarray
|
|
21
|
+
1-D signal.
|
|
22
|
+
sf : float
|
|
23
|
+
Sampling frequency.
|
|
24
|
+
band : list, optional
|
|
25
|
+
Frequency band of interest, e.g. ``[0.5, 30]``. Default is ``[0.5, 30]``.
|
|
26
|
+
relative : bool
|
|
27
|
+
Whether to normalize the PSD so it integrates to 1.
|
|
28
|
+
win_sec : int
|
|
29
|
+
Window length (in seconds) for the FFT.
|
|
30
|
+
nfft : int, optional
|
|
31
|
+
Number of FFT points.
|
|
32
|
+
gaussian_sigma : float, optional
|
|
33
|
+
Sigma for Gaussian smoothing of the PSD.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
freq : ndarray
|
|
38
|
+
Frequency bins.
|
|
39
|
+
psd : ndarray
|
|
40
|
+
Power spectral density.
|
|
41
|
+
"""
|
|
42
|
+
if not isinstance(signal, np.ndarray):
|
|
43
|
+
raise TypeError(f"'signal' should be a numpy array, got {type(signal)}")
|
|
44
|
+
if not isinstance(sf, (int, float)):
|
|
45
|
+
raise TypeError(f"'sf' should be an integer or float, got {type(sf)}")
|
|
46
|
+
if not isinstance(relative, bool):
|
|
47
|
+
raise TypeError("'relative' should be a boolean")
|
|
48
|
+
|
|
49
|
+
if band is None:
|
|
50
|
+
band = [0.5, 30]
|
|
51
|
+
if not isinstance(band, list):
|
|
52
|
+
raise TypeError(f"'band' should be a list, e.g. [0.5, 4], got {type(band)}")
|
|
53
|
+
|
|
54
|
+
signal, _ = signal_filter(data=signal, sf=sf, btype="bandpass", low=band[0], high=band[1])
|
|
55
|
+
|
|
56
|
+
freq, psd = welch(signal, sf, nperseg=int(sf * win_sec), nfft=nfft, scaling="density")
|
|
57
|
+
freq = np.array([round(each, 2) for each in freq])
|
|
58
|
+
psd = gaussian_filter1d(psd, sigma=gaussian_sigma) if gaussian_sigma is not None else psd
|
|
59
|
+
|
|
60
|
+
idx_freq = np.logical_and(freq >= band[0], freq <= band[1])
|
|
61
|
+
freq = freq[idx_freq]
|
|
62
|
+
psd = psd[idx_freq]
|
|
63
|
+
|
|
64
|
+
total_power = simpson(psd, dx=freq[1] - freq[0])
|
|
65
|
+
if relative and total_power > 0:
|
|
66
|
+
psd /= total_power
|
|
67
|
+
|
|
68
|
+
return freq, psd
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def spectrogram(signal, sf, band=None, step=0.2, win_sec=2, norm=False, nfft=None):
|
|
72
|
+
"""Calculate the spectrogram of a signal with the STFT.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
signal : ndarray
|
|
77
|
+
1-D signal.
|
|
78
|
+
sf : float
|
|
79
|
+
Sampling frequency.
|
|
80
|
+
band : list, optional
|
|
81
|
+
Frequency band of interest, e.g. ``[0.5, 30]``.
|
|
82
|
+
step : float
|
|
83
|
+
Step (in seconds) between STFT windows.
|
|
84
|
+
win_sec : float
|
|
85
|
+
Window length (in seconds) for the STFT.
|
|
86
|
+
norm : bool
|
|
87
|
+
Whether to normalize the power across frequencies at each time point.
|
|
88
|
+
nfft : int, optional
|
|
89
|
+
Number of FFT points.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
f : ndarray
|
|
94
|
+
Frequency bins.
|
|
95
|
+
t : ndarray
|
|
96
|
+
Time bins.
|
|
97
|
+
Sxx : ndarray
|
|
98
|
+
Spectrogram (squared magnitude of the STFT).
|
|
99
|
+
"""
|
|
100
|
+
if not isinstance(signal, np.ndarray):
|
|
101
|
+
raise TypeError(f"'signal' should be a numpy array, got {type(signal)}")
|
|
102
|
+
if not isinstance(sf, (int, float)):
|
|
103
|
+
raise TypeError(f"'sf' should be an integer or float, got {type(sf)}")
|
|
104
|
+
|
|
105
|
+
if step > win_sec:
|
|
106
|
+
raise ValueError(f"'step' ({step}) should be smaller than 'win_sec' ({win_sec})")
|
|
107
|
+
step = 1 / sf if step <= 0 else step
|
|
108
|
+
|
|
109
|
+
if not isinstance(norm, bool):
|
|
110
|
+
raise TypeError("'norm' should be a boolean")
|
|
111
|
+
|
|
112
|
+
if band is None:
|
|
113
|
+
band = [0.5, 30]
|
|
114
|
+
if not isinstance(band, list):
|
|
115
|
+
raise TypeError(f"'band' should be a list, e.g. [0.5, 30], got {type(band)}")
|
|
116
|
+
|
|
117
|
+
nperseg = int(win_sec * sf)
|
|
118
|
+
noverlap = int(nperseg - (step * sf))
|
|
119
|
+
|
|
120
|
+
f, t, Sxx = stft(signal, sf, nperseg=nperseg, nfft=nfft,
|
|
121
|
+
noverlap=noverlap, padded=False, boundary="zeros")
|
|
122
|
+
|
|
123
|
+
f = np.array([round(each, 2) for each in f])
|
|
124
|
+
|
|
125
|
+
idx_f = np.logical_and(f >= band[0], f <= band[1])
|
|
126
|
+
f = f[idx_f]
|
|
127
|
+
Sxx = Sxx[idx_f, :]
|
|
128
|
+
Sxx = np.square(np.abs(Sxx))
|
|
129
|
+
|
|
130
|
+
if norm:
|
|
131
|
+
sum_power = Sxx.sum(0).reshape(1, -1)
|
|
132
|
+
np.divide(Sxx, sum_power, out=Sxx, where=sum_power != 0)
|
|
133
|
+
|
|
134
|
+
return f, t, Sxx
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def band_power(psd, freq, bands=None, relative=False):
|
|
138
|
+
"""Compute the band power of a PSD.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
psd : ndarray
|
|
143
|
+
Power spectral density values.
|
|
144
|
+
freq : ndarray
|
|
145
|
+
Frequencies corresponding to ``psd``.
|
|
146
|
+
bands : list, optional
|
|
147
|
+
Frequency bands, e.g. ``[[0.5, 4, 'delta'], [4, 9, 'theta']]``.
|
|
148
|
+
relative : bool
|
|
149
|
+
Whether to express each band power relative to the total power.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
dict
|
|
154
|
+
Band name -> band power.
|
|
155
|
+
"""
|
|
156
|
+
freq_res = freq[1] - freq[0]
|
|
157
|
+
band_dict = {}
|
|
158
|
+
for each in bands:
|
|
159
|
+
idx_band = np.logical_and(freq >= each[0], freq <= each[1])
|
|
160
|
+
bp = simpson(psd[idx_band], dx=freq_res)
|
|
161
|
+
|
|
162
|
+
if relative:
|
|
163
|
+
total = simpson(psd, dx=freq_res)
|
|
164
|
+
if total > 0:
|
|
165
|
+
bp /= total
|
|
166
|
+
|
|
167
|
+
band_dict[each[2]] = bp
|
|
168
|
+
|
|
169
|
+
return band_dict
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Utility module.
|
|
3
|
+
|
|
4
|
+
Pure-Python helpers that do not depend on the GUI or the data containers:
|
|
5
|
+
|
|
6
|
+
* :mod:`misleep.utils.annotation` -- annotation line <-> list conversion
|
|
7
|
+
* :mod:`misleep.utils.time_utils` -- datetime helpers
|
|
8
|
+
* :mod:`misleep.utils.entropy` -- signal complexity measures
|
|
9
|
+
* :mod:`misleep.utils.misc` -- various small helpers
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .annotation import (
|
|
13
|
+
lst2group,
|
|
14
|
+
marker2mianno,
|
|
15
|
+
start_end2mianno,
|
|
16
|
+
sleep_state2mianno,
|
|
17
|
+
insert_row,
|
|
18
|
+
temp_loop4below_row,
|
|
19
|
+
)
|
|
20
|
+
from .time_utils import transfer_time, second2time
|
|
21
|
+
from .entropy import num_zerocross, hjorth_params, perm_entropy
|
|
22
|
+
from .misc import (
|
|
23
|
+
create_new_mianno,
|
|
24
|
+
identify_startend_color,
|
|
25
|
+
get_base_path,
|
|
26
|
+
downsample_by_most_frequent,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"lst2group",
|
|
31
|
+
"marker2mianno",
|
|
32
|
+
"start_end2mianno",
|
|
33
|
+
"sleep_state2mianno",
|
|
34
|
+
"insert_row",
|
|
35
|
+
"temp_loop4below_row",
|
|
36
|
+
"transfer_time",
|
|
37
|
+
"second2time",
|
|
38
|
+
"num_zerocross",
|
|
39
|
+
"hjorth_params",
|
|
40
|
+
"perm_entropy",
|
|
41
|
+
"create_new_mianno",
|
|
42
|
+
"identify_startend_color",
|
|
43
|
+
"get_base_path",
|
|
44
|
+
"downsample_by_most_frequent",
|
|
45
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Helpers for converting between annotation representations."""
|
|
3
|
+
|
|
4
|
+
from misleep.utils.time_utils import transfer_time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def lst2group(pre_lst):
|
|
8
|
+
"""Group consecutive rows of a two-column list into ``[start, end, value]`` triples.
|
|
9
|
+
|
|
10
|
+
Converts a list like ``[[1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 1], [7, 1], ...]``
|
|
11
|
+
into ``[[1, 6, 2], [6, 9, 1], ...]`` where the second element of each
|
|
12
|
+
triple is an *exclusive* end index.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
pre_lst : list of [index, value]
|
|
17
|
+
Sorted list of ``[index, value]`` pairs.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
list of [start, end, value]
|
|
22
|
+
Consecutive runs, ``end`` is exclusive.
|
|
23
|
+
"""
|
|
24
|
+
rows = iter(pre_lst)
|
|
25
|
+
try:
|
|
26
|
+
first_idx, first_value = next(rows)
|
|
27
|
+
except StopIteration:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
grouped = []
|
|
31
|
+
run_start = last_idx = first_idx
|
|
32
|
+
run_value = first_value
|
|
33
|
+
for idx, value in rows:
|
|
34
|
+
if value != run_value:
|
|
35
|
+
grouped.append([run_start, last_idx + 1, run_value])
|
|
36
|
+
run_start = idx
|
|
37
|
+
run_value = value
|
|
38
|
+
last_idx = idx
|
|
39
|
+
grouped.append([run_start, last_idx + 1, run_value])
|
|
40
|
+
return grouped
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def marker2mianno(marker):
|
|
44
|
+
"""Convert MiSleep annotation lines to ``[[time, label], ...]`` markers."""
|
|
45
|
+
if marker != [] or marker is not None:
|
|
46
|
+
marker = [each.split(", ") for each in marker]
|
|
47
|
+
marker = [[float(each[1]), each[7]] for each in marker]
|
|
48
|
+
return marker
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def start_end2mianno(start_end):
|
|
53
|
+
"""Convert MiSleep annotation lines to ``[[start, end, label], ...]`` events."""
|
|
54
|
+
if start_end != [] or start_end is not None:
|
|
55
|
+
start_end = [each.split(", ") for each in start_end]
|
|
56
|
+
start_end = [[float(each[1]), float(each[4]), each[7]] for each in start_end]
|
|
57
|
+
return start_end
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sleep_state2mianno(sleep_state):
|
|
62
|
+
"""Convert MiSleep annotation lines to a per-second state sequence."""
|
|
63
|
+
start_end = [each.split(", ") for each in sleep_state]
|
|
64
|
+
# Old version misleep labels start from 1
|
|
65
|
+
if start_end[0][1] == "1":
|
|
66
|
+
sleep_state = [item for each in start_end for item in [int(each[6])] * (int(each[4]) - int(each[1]) + 1)]
|
|
67
|
+
else:
|
|
68
|
+
sleep_state = [item for each in start_end for item in [int(each[6])] * (int(each[4]) - int(each[1]))]
|
|
69
|
+
return sleep_state
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def insert_row(df, idx, row):
|
|
73
|
+
"""Insert a row into a dataframe at a specific position.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
df : pandas.DataFrame
|
|
78
|
+
Dataframe to insert into.
|
|
79
|
+
idx : int
|
|
80
|
+
Position to insert the row (inserted below this index).
|
|
81
|
+
row : pandas.Series or DataFrame
|
|
82
|
+
Row to insert.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
pandas.DataFrame
|
|
87
|
+
A new dataframe with the row inserted.
|
|
88
|
+
"""
|
|
89
|
+
import pandas as pd
|
|
90
|
+
|
|
91
|
+
if isinstance(row, pd.Series):
|
|
92
|
+
row = pd.DataFrame(row).T
|
|
93
|
+
df = pd.concat([df[:idx], row, df[idx:]], axis=0).reset_index(drop=True)
|
|
94
|
+
return df
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def temp_loop4below_row(row, acquisition_time, columns):
|
|
98
|
+
"""Split a row that crosses an hour boundary into three rows.
|
|
99
|
+
|
|
100
|
+
Used when exporting per-hour sleep statistics: a long bout that
|
|
101
|
+
straddles a full hour is split into "before hour", an hourly
|
|
102
|
+
``MARKER`` divider row, and "after hour".
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
(previous_row, new_row, below_row) : tuple of pandas.Series
|
|
107
|
+
"""
|
|
108
|
+
import pandas as pd
|
|
109
|
+
|
|
110
|
+
seconds_ = (int(row["start_time_sec"] / 3600) + 1) * 3600
|
|
111
|
+
previous_row = pd.Series([
|
|
112
|
+
row["start_time"], row["start_time_sec"], "1",
|
|
113
|
+
transfer_time(acquisition_time, seconds_, "%Y-%m-%d %H:%M:%S"),
|
|
114
|
+
seconds_, "0", row["state_code"], row["state"]
|
|
115
|
+
], index=columns)
|
|
116
|
+
|
|
117
|
+
new_row = pd.Series([
|
|
118
|
+
transfer_time(acquisition_time, seconds_, "%Y-%m-%d %H:%M:%S"),
|
|
119
|
+
seconds_, " ",
|
|
120
|
+
transfer_time(acquisition_time, seconds_, "%Y-%m-%d %H:%M:%S"),
|
|
121
|
+
seconds_, " ", "5", "MARKER"
|
|
122
|
+
], index=columns)
|
|
123
|
+
|
|
124
|
+
below_row = pd.Series([
|
|
125
|
+
transfer_time(acquisition_time, seconds_ + 1, "%Y-%m-%d %H:%M:%S"),
|
|
126
|
+
seconds_ + 1, "1", row["end_time"], row["end_time_sec"],
|
|
127
|
+
"0", row["state_code"], row["state"]
|
|
128
|
+
], index=columns)
|
|
129
|
+
|
|
130
|
+
return previous_row, new_row, below_row
|
misleep/utils/entropy.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Self-contained implementations of a few signal-complexity measures.
|
|
3
|
+
|
|
4
|
+
These functions were originally copied from the `antropy` package
|
|
5
|
+
(https://github.com/raphaelvallat/antropy, BSD-3-Clause) so that MiSleep
|
|
6
|
+
does not need to import the whole package just for three functions --
|
|
7
|
+
importing antropy used to cost a lot of startup time.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from math import factorial
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def num_zerocross(x, normalize=False, axis=-1):
|
|
15
|
+
"""Number of zero-crossings of a signal.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
x : array_like
|
|
20
|
+
1-D or N-D data.
|
|
21
|
+
normalize : bool
|
|
22
|
+
If True, divide by the number of samples (output in ``[0, 1]``).
|
|
23
|
+
axis : int
|
|
24
|
+
Axis along which to compute. Default is -1.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
nzc : int or ndarray
|
|
29
|
+
Number of zero-crossings.
|
|
30
|
+
"""
|
|
31
|
+
x = np.asarray(x)
|
|
32
|
+
nzc = np.diff(np.signbit(x), axis=axis).sum(axis=axis)
|
|
33
|
+
if normalize:
|
|
34
|
+
nzc = nzc / x.shape[axis]
|
|
35
|
+
return nzc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hjorth_params(x, axis=-1):
|
|
39
|
+
"""Calculate Hjorth mobility and complexity on the given axis.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
x : array_like
|
|
44
|
+
1-D or N-D data.
|
|
45
|
+
axis : int
|
|
46
|
+
Axis along which to compute. Default is -1.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
mobility, complexity : float or ndarray
|
|
51
|
+
Hjorth mobility and complexity parameters.
|
|
52
|
+
"""
|
|
53
|
+
x = np.asarray(x)
|
|
54
|
+
dx = np.diff(x, axis=axis)
|
|
55
|
+
ddx = np.diff(dx, axis=axis)
|
|
56
|
+
x_var = np.var(x, axis=axis) # = activity
|
|
57
|
+
dx_var = np.var(dx, axis=axis)
|
|
58
|
+
ddx_var = np.var(ddx, axis=axis)
|
|
59
|
+
mob = np.sqrt(dx_var / x_var)
|
|
60
|
+
com = np.sqrt(ddx_var / dx_var) / mob
|
|
61
|
+
return mob, com
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def perm_entropy(x, order=3, delay=1, normalize=False):
|
|
65
|
+
"""Permutation entropy (Bandt & Pompe, 2002).
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
x : array_like
|
|
70
|
+
1-D time series.
|
|
71
|
+
order : int
|
|
72
|
+
Order of permutation entropy (embedding dimension). Default is 3.
|
|
73
|
+
delay : int or array_like
|
|
74
|
+
Time delay (lag). When several delays are given, the average
|
|
75
|
+
permutation entropy across them is returned.
|
|
76
|
+
normalize : bool
|
|
77
|
+
If True, divide by ``log2(order!)`` so the output lies in [0, 1].
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
pe : float
|
|
82
|
+
Permutation entropy (in bits, unless normalized).
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(delay, (list, np.ndarray, range)):
|
|
85
|
+
return np.mean([perm_entropy(x, order=order, delay=d, normalize=normalize) for d in delay])
|
|
86
|
+
x = np.array(x)
|
|
87
|
+
ran_order = range(order)
|
|
88
|
+
hashmult = np.power(order, ran_order)
|
|
89
|
+
assert delay > 0, "delay must be greater than zero."
|
|
90
|
+
sorted_idx = _embed(x, order=order, delay=delay).argsort(kind="quicksort")
|
|
91
|
+
hashval = (np.multiply(sorted_idx, hashmult)).sum(1)
|
|
92
|
+
_, c = np.unique(hashval, return_counts=True)
|
|
93
|
+
p = np.true_divide(c, c.sum())
|
|
94
|
+
pe = -_xlogx(p).sum()
|
|
95
|
+
if normalize:
|
|
96
|
+
pe /= np.log2(factorial(order))
|
|
97
|
+
return pe
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _embed(x, order=3, delay=1):
|
|
101
|
+
"""Time-delay embedding of a 1-D or 2-D array."""
|
|
102
|
+
x = np.asarray(x)
|
|
103
|
+
N = x.shape[-1]
|
|
104
|
+
assert x.ndim in [1, 2], "Only 1D or 2D arrays are currently supported."
|
|
105
|
+
if order * delay > N:
|
|
106
|
+
raise ValueError("Error: order * delay should be lower than x.size")
|
|
107
|
+
if delay < 1:
|
|
108
|
+
raise ValueError("Delay has to be at least 1.")
|
|
109
|
+
if order < 2:
|
|
110
|
+
raise ValueError("Order has to be at least 2.")
|
|
111
|
+
|
|
112
|
+
if x.ndim == 1:
|
|
113
|
+
Y = np.zeros((order, N - (order - 1) * delay))
|
|
114
|
+
for i in range(order):
|
|
115
|
+
Y[i] = x[(i * delay): (i * delay + Y.shape[1])]
|
|
116
|
+
return Y.T
|
|
117
|
+
else:
|
|
118
|
+
Y = []
|
|
119
|
+
embed_signal_length = N - (order - 1) * delay
|
|
120
|
+
indice = [[(i * delay), (i * delay + embed_signal_length)] for i in range(order)]
|
|
121
|
+
for i in range(order):
|
|
122
|
+
temp = x[:, indice[i][0]: indice[i][1]].reshape(-1, embed_signal_length, 1)
|
|
123
|
+
Y.append(temp)
|
|
124
|
+
Y = np.concatenate(Y, axis=-1)
|
|
125
|
+
return Y
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _xlogx(x, base=2):
|
|
129
|
+
"""Return ``x * log_b(x)`` for positive x, 0 for x == 0, else nan."""
|
|
130
|
+
x = np.asarray(x)
|
|
131
|
+
xlogx = np.zeros(x.shape)
|
|
132
|
+
xlogx[x < 0] = np.nan
|
|
133
|
+
valid = x > 0
|
|
134
|
+
xlogx[valid] = x[valid] * np.log(x[valid]) / np.log(base)
|
|
135
|
+
return xlogx
|
misleep/utils/misc.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Miscellaneous helpers shared across MiSleep."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
from misleep.data.annotation import MiAnnotation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_new_mianno(data_duration):
|
|
11
|
+
"""Create a fresh :class:`MiAnnotation` with all states set to Init (4).
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
data_duration : int
|
|
16
|
+
Duration of the recording in seconds.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
MiAnnotation
|
|
21
|
+
"""
|
|
22
|
+
marker = []
|
|
23
|
+
start_end = []
|
|
24
|
+
sleep_state = [4 for _ in range(data_duration)]
|
|
25
|
+
return MiAnnotation(sleep_state=sleep_state, start_end=start_end, marker=marker)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def identify_startend_color(color_dict, state_name, end=False):
|
|
29
|
+
"""Return the color of a state name from a mapping, or ``'blue'`` as fallback."""
|
|
30
|
+
if state_name in color_dict.keys():
|
|
31
|
+
return color_dict[state_name]
|
|
32
|
+
return "blue"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_base_path(file_path):
|
|
36
|
+
"""Strip a known data-file extension from a path.
|
|
37
|
+
|
|
38
|
+
Returns the path without ``.txt``/``.xlsx``/``.mat``/``.edf``/``.csv``
|
|
39
|
+
when present, otherwise the input unchanged.
|
|
40
|
+
"""
|
|
41
|
+
base, ext = os.path.splitext(file_path)
|
|
42
|
+
if ext in [".txt", ".xlsx", ".mat", ".edf", ".csv"]:
|
|
43
|
+
return base
|
|
44
|
+
return file_path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def downsample_by_most_frequent(lst, group_size=5):
|
|
48
|
+
"""Downsample a list by taking the most frequent element per chunk.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
lst : list
|
|
53
|
+
Input sequence.
|
|
54
|
+
group_size : int
|
|
55
|
+
Chunk size. Default is 5.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
list
|
|
60
|
+
One representative value per chunk.
|
|
61
|
+
"""
|
|
62
|
+
result = []
|
|
63
|
+
for i in range(0, len(lst), group_size):
|
|
64
|
+
group = lst[i:i + group_size]
|
|
65
|
+
if group:
|
|
66
|
+
counter = Counter(group)
|
|
67
|
+
most_common = counter.most_common(1)[0][0]
|
|
68
|
+
result.append(most_common)
|
|
69
|
+
return result
|