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.
Files changed (94) hide show
  1. misleep/__init__.py +111 -0
  2. misleep/__main__.py +16 -0
  3. misleep/analysis/__init__.py +23 -0
  4. misleep/analysis/auto_stage.py +158 -0
  5. misleep/analysis/detection.py +198 -0
  6. misleep/analysis/features.py +131 -0
  7. misleep/analysis/models/P30_EEG_F_lightgbm.pkl +0 -0
  8. misleep/analysis/models/P30_EEG_P_lightgbm.pkl +0 -0
  9. misleep/analysis/models/__init__.py +0 -0
  10. misleep/analysis/models/ado_EEG_F_lightgbm.pkl +0 -0
  11. misleep/analysis/models/ado_EEG_P_lightgbm.pkl +0 -0
  12. misleep/analysis/models/adult_EEG_F_lightgbm.pkl +0 -0
  13. misleep/analysis/models/adult_EEG_P_lightgbm.pkl +0 -0
  14. misleep/analysis/transformer/__init__.py +46 -0
  15. misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
  16. misleep/analysis/transformer/checkpoints/__init__.py +0 -0
  17. misleep/analysis/transformer/configs.py +52 -0
  18. misleep/analysis/transformer/data.py +362 -0
  19. misleep/analysis/transformer/inference.py +328 -0
  20. misleep/analysis/transformer/models.py +220 -0
  21. misleep/analysis/transformer/preprocessing.py +105 -0
  22. misleep/config/__init__.py +105 -0
  23. misleep/config/default_config.ini +38 -0
  24. misleep/data/__init__.py +13 -0
  25. misleep/data/annotation.py +114 -0
  26. misleep/data/midata.py +387 -0
  27. misleep/gui/__init__.py +23 -0
  28. misleep/gui/app.py +183 -0
  29. misleep/gui/config_dialog.py +429 -0
  30. misleep/gui/dialogs.py +1107 -0
  31. misleep/gui/event_filters.py +26 -0
  32. misleep/gui/main_window.py +2699 -0
  33. misleep/gui/qt_utils.py +277 -0
  34. misleep/gui/resources/__init__.py +9 -0
  35. misleep/gui/resources/entire_logo.png +0 -0
  36. misleep/gui/resources/logo.png +0 -0
  37. misleep/gui/resources/misleep.ico +0 -0
  38. misleep/gui/resources/misleep.qrc +7 -0
  39. misleep/gui/resources/misleep_rc.py +1324 -0
  40. misleep/gui/spec_window.py +187 -0
  41. misleep/gui/style.py +994 -0
  42. misleep/gui/uis/SWA_detect_dialog.ui +181 -0
  43. misleep/gui/uis/SWA_detect_dialog_ui.py +176 -0
  44. misleep/gui/uis/__init__.py +6 -0
  45. misleep/gui/uis/about.ui +88 -0
  46. misleep/gui/uis/about_ui.py +80 -0
  47. misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +77 -0
  48. misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +90 -0
  49. misleep/gui/uis/auto_stage_lightGBM_dialog.ui +124 -0
  50. misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +124 -0
  51. misleep/gui/uis/horizontal_line_dialog.ui +166 -0
  52. misleep/gui/uis/horizontal_line_dialog_ui.py +157 -0
  53. misleep/gui/uis/label_dialog.ui +65 -0
  54. misleep/gui/uis/label_dialog_ui.py +72 -0
  55. misleep/gui/uis/main_window.ui +871 -0
  56. misleep/gui/uis/main_window_ui.py +616 -0
  57. misleep/gui/uis/save_data_dialog.ui +95 -0
  58. misleep/gui/uis/save_data_dialog_ui.py +98 -0
  59. misleep/gui/uis/spec_window.ui +109 -0
  60. misleep/gui/uis/spec_window_ui.py +103 -0
  61. misleep/gui/uis/spindle_detect_dialog.ui +202 -0
  62. misleep/gui/uis/spindle_detect_dialog_ui.py +194 -0
  63. misleep/gui/uis/state_spectral_dialog.ui +240 -0
  64. misleep/gui/uis/state_spectral_dialog_ui.py +204 -0
  65. misleep/gui/uis/transfer_result_dialog.ui +104 -0
  66. misleep/gui/uis/transfer_result_dialog_ui.py +94 -0
  67. misleep/gui/workers.py +69 -0
  68. misleep/io/__init__.py +68 -0
  69. misleep/io/annotation.py +367 -0
  70. misleep/io/array.py +209 -0
  71. misleep/io/base.py +174 -0
  72. misleep/io/edf.py +109 -0
  73. misleep/io/mat.py +189 -0
  74. misleep/logger.py +79 -0
  75. misleep/preprocessing/__init__.py +23 -0
  76. misleep/preprocessing/artifacts.py +76 -0
  77. misleep/preprocessing/filtering.py +94 -0
  78. misleep/preprocessing/segment.py +59 -0
  79. misleep/preprocessing/spectral.py +169 -0
  80. misleep/utils/__init__.py +45 -0
  81. misleep/utils/annotation.py +130 -0
  82. misleep/utils/entropy.py +135 -0
  83. misleep/utils/misc.py +69 -0
  84. misleep/utils/time_utils.py +61 -0
  85. misleep/viz/__init__.py +12 -0
  86. misleep/viz/hypnogram.py +48 -0
  87. misleep/viz/signals.py +51 -0
  88. misleep/viz/spectral.py +98 -0
  89. misleep-0.3.0.dist-info/METADATA +249 -0
  90. misleep-0.3.0.dist-info/RECORD +94 -0
  91. misleep-0.3.0.dist-info/WHEEL +5 -0
  92. misleep-0.3.0.dist-info/entry_points.txt +15 -0
  93. misleep-0.3.0.dist-info/licenses/LICENSE +29 -0
  94. 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
@@ -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