accusleepy 0.1.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.
- accusleepy/__init__.py +0 -0
- accusleepy/__main__.py +4 -0
- accusleepy/brain_state_set.py +89 -0
- accusleepy/classification.py +267 -0
- accusleepy/config.json +22 -0
- accusleepy/constants.py +37 -0
- accusleepy/fileio.py +201 -0
- accusleepy/gui/__init__.py +0 -0
- accusleepy/gui/icons/brightness_down.png +0 -0
- accusleepy/gui/icons/brightness_up.png +0 -0
- accusleepy/gui/icons/double_down_arrow.png +0 -0
- accusleepy/gui/icons/double_up_arrow.png +0 -0
- accusleepy/gui/icons/down_arrow.png +0 -0
- accusleepy/gui/icons/home.png +0 -0
- accusleepy/gui/icons/question.png +0 -0
- accusleepy/gui/icons/save.png +0 -0
- accusleepy/gui/icons/up_arrow.png +0 -0
- accusleepy/gui/icons/zoom_in.png +0 -0
- accusleepy/gui/icons/zoom_out.png +0 -0
- accusleepy/gui/main.py +1372 -0
- accusleepy/gui/manual_scoring.py +1086 -0
- accusleepy/gui/mplwidget.py +356 -0
- accusleepy/gui/primary_window.py +2330 -0
- accusleepy/gui/primary_window.ui +3432 -0
- accusleepy/gui/resources.qrc +16 -0
- accusleepy/gui/resources_rc.py +6710 -0
- accusleepy/gui/text/config_guide.txt +24 -0
- accusleepy/gui/text/main_guide.txt +142 -0
- accusleepy/gui/text/manual_scoring_guide.txt +28 -0
- accusleepy/gui/viewer_window.py +598 -0
- accusleepy/gui/viewer_window.ui +894 -0
- accusleepy/models.py +48 -0
- accusleepy/multitaper.py +659 -0
- accusleepy/signal_processing.py +589 -0
- accusleepy-0.1.0.dist-info/METADATA +57 -0
- accusleepy-0.1.0.dist-info/RECORD +37 -0
- accusleepy-0.1.0.dist-info/WHEEL +4 -0
accusleepy/models.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from torch import nn
|
|
5
|
+
|
|
6
|
+
from accusleepy.constants import (
|
|
7
|
+
DOWNSAMPLING_START_FREQ,
|
|
8
|
+
EMG_COPIES,
|
|
9
|
+
MIN_WINDOW_LEN,
|
|
10
|
+
UPPER_FREQ,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# height in pixels of each training image
|
|
14
|
+
IMAGE_HEIGHT = (
|
|
15
|
+
len(np.arange(0, DOWNSAMPLING_START_FREQ, 1 / MIN_WINDOW_LEN))
|
|
16
|
+
+ len(np.arange(DOWNSAMPLING_START_FREQ, UPPER_FREQ, 2 / MIN_WINDOW_LEN))
|
|
17
|
+
+ EMG_COPIES
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SSANN(nn.Module):
|
|
22
|
+
"""Small CNN for classifying images"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, n_classes: int):
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
self.pool = nn.MaxPool2d(2, 2)
|
|
28
|
+
self.conv1 = nn.Conv2d(
|
|
29
|
+
in_channels=1, out_channels=8, kernel_size=3, padding="same"
|
|
30
|
+
)
|
|
31
|
+
self.conv2 = nn.Conv2d(
|
|
32
|
+
in_channels=8, out_channels=16, kernel_size=3, padding="same"
|
|
33
|
+
)
|
|
34
|
+
self.conv3 = nn.Conv2d(
|
|
35
|
+
in_channels=16, out_channels=32, kernel_size=3, padding="same"
|
|
36
|
+
)
|
|
37
|
+
self.conv1_bn = nn.BatchNorm2d(8)
|
|
38
|
+
self.conv2_bn = nn.BatchNorm2d(16)
|
|
39
|
+
self.conv3_bn = nn.BatchNorm2d(32)
|
|
40
|
+
self.fc1 = nn.Linear(int(32 * IMAGE_HEIGHT / 8), n_classes)
|
|
41
|
+
|
|
42
|
+
def forward(self, x):
|
|
43
|
+
x = x.float()
|
|
44
|
+
x = self.pool(F.relu(self.conv1_bn(self.conv1(x))))
|
|
45
|
+
x = self.pool(F.relu(self.conv2_bn(self.conv2(x))))
|
|
46
|
+
x = self.pool(F.relu(self.conv3_bn(self.conv3(x))))
|
|
47
|
+
x = torch.flatten(x, 1) # flatten all dimensions except batch
|
|
48
|
+
return self.fc1(x)
|
accusleepy/multitaper.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is entirely written by the Prerau Lab. Retrieved from this URL in 2025:
|
|
3
|
+
https://github.com/preraulab/multitaper_toolbox/blob/master/python/multitaper_spectrogram_python.py
|
|
4
|
+
Only minor modifications have been made.
|
|
5
|
+
For more information, see their publication:
|
|
6
|
+
"Sleep Neurophysiological Dynamics Through the Lens of Multitaper Spectral Analysis"
|
|
7
|
+
Michael J. Prerau, Ritchie E. Brown, Matt T. Bianchi, Jeffrey M. Ellenbogen, Patrick L. Purdon
|
|
8
|
+
December 7, 2016 : 60-92
|
|
9
|
+
DOI: 10.1152/physiol.00062.2015
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
import timeit
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from joblib import Parallel, cpu_count, delayed
|
|
18
|
+
from scipy.signal import detrend
|
|
19
|
+
from scipy.signal.windows import dpss
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# MULTITAPER SPECTROGRAM #
|
|
23
|
+
def spectrogram(
|
|
24
|
+
data,
|
|
25
|
+
fs,
|
|
26
|
+
frequency_range=None,
|
|
27
|
+
time_bandwidth=5,
|
|
28
|
+
num_tapers=None,
|
|
29
|
+
window_params=None,
|
|
30
|
+
min_nfft=0,
|
|
31
|
+
detrend_opt="linear",
|
|
32
|
+
multiprocess=False,
|
|
33
|
+
n_jobs=None,
|
|
34
|
+
weighting="unity",
|
|
35
|
+
plot_on=False,
|
|
36
|
+
return_fig=False,
|
|
37
|
+
clim_scale=True,
|
|
38
|
+
verbose=True,
|
|
39
|
+
xyflip=False,
|
|
40
|
+
ax=None,
|
|
41
|
+
):
|
|
42
|
+
"""Compute multitaper spectrogram of timeseries data
|
|
43
|
+
Usage:
|
|
44
|
+
mt_spectrogram, stimes, sfreqs = spectrogram(data, fs, frequency_range=None, time_bandwidth=5,
|
|
45
|
+
num_tapers=None, window_params=None, min_nfft=0,
|
|
46
|
+
detrend_opt='linear', multiprocess=False, cpus=False,
|
|
47
|
+
weighting='unity', plot_on=True, return_fig=False,
|
|
48
|
+
clim_scale=True, verbose=True, xyflip=False):
|
|
49
|
+
Arguments:
|
|
50
|
+
data (1d np.array): time series data -- required
|
|
51
|
+
fs (float): sampling frequency in Hz -- required
|
|
52
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] (default: [0 nyquist])
|
|
53
|
+
time_bandwidth (float): time-half bandwidth product (window duration*half bandwidth of main lobe)
|
|
54
|
+
(default: 5 Hz*s)
|
|
55
|
+
num_tapers (int): number of DPSS tapers to use (default: [will be computed
|
|
56
|
+
as floor(2*time_bandwidth - 1)])
|
|
57
|
+
window_params (list): 1x2 list - [window size (seconds), step size (seconds)] (default: [5 1])
|
|
58
|
+
detrend_opt (string): detrend data window ('linear' (default), 'constant', 'off')
|
|
59
|
+
(Default: 'linear')
|
|
60
|
+
min_nfft (int): minimum allowable NFFT size, adds zero padding for interpolation (closest 2^x)
|
|
61
|
+
(default: 0)
|
|
62
|
+
multiprocess (bool): Use multiprocessing to compute multitaper spectrogram (default: False)
|
|
63
|
+
n_jobs (int): Number of cpus to use if multiprocess = True (default: False). Note: if default is left
|
|
64
|
+
as None and multiprocess = True, the number of cpus used for multiprocessing will be
|
|
65
|
+
all available - 1.
|
|
66
|
+
weighting (str): weighting of tapers ('unity' (default), 'eigen', 'adapt');
|
|
67
|
+
plot_on (bool): plot results (default: True)
|
|
68
|
+
return_fig (bool): return plotted spectrogram (default: False)
|
|
69
|
+
clim_scale (bool): automatically scale the colormap on the plotted spectrogram (default: True)
|
|
70
|
+
verbose (bool): display spectrogram properties (default: True)
|
|
71
|
+
xyflip (bool): transpose the mt_spectrogram output (default: False)
|
|
72
|
+
ax (axes): a matplotlib axes to plot the spectrogram on (default: None)
|
|
73
|
+
Returns:
|
|
74
|
+
mt_spectrogram (TxF np array): spectral power matrix
|
|
75
|
+
stimes (1xT np array): timepoints (s) in mt_spectrogram
|
|
76
|
+
sfreqs (1xF np array)L frequency values (Hz) in mt_spectrogram
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
In this example we create some chirp data and run the multitaper spectrogram on it.
|
|
80
|
+
import numpy as np # import numpy
|
|
81
|
+
from scipy.signal import chirp # import chirp generation function
|
|
82
|
+
# Set spectrogram params
|
|
83
|
+
fs = 200 # Sampling Frequency
|
|
84
|
+
frequency_range = [0, 25] # Limit frequencies from 0 to 25 Hz
|
|
85
|
+
time_bandwidth = 3 # Set time-half bandwidth
|
|
86
|
+
num_tapers = 5 # Set number of tapers (optimal is time_bandwidth*2 - 1)
|
|
87
|
+
window_params = [4, 1] # Window size is 4s with step size of 1s
|
|
88
|
+
min_nfft = 0 # No minimum nfft
|
|
89
|
+
detrend_opt = 'constant' # detrend each window by subtracting the average
|
|
90
|
+
multiprocess = True # use multiprocessing
|
|
91
|
+
cpus = 3 # use 3 cores in multiprocessing
|
|
92
|
+
weighting = 'unity' # weight each taper at 1
|
|
93
|
+
plot_on = True # plot spectrogram
|
|
94
|
+
return_fig = False # do not return plotted spectrogram
|
|
95
|
+
clim_scale = False # don't auto-scale the colormap
|
|
96
|
+
verbose = True # print extra info
|
|
97
|
+
xyflip = False # do not transpose spect output matrix
|
|
98
|
+
|
|
99
|
+
# Generate sample chirp data
|
|
100
|
+
t = np.arange(1/fs, 600, 1/fs) # Create 10 min time array from 1/fs to 600 stepping by 1/fs
|
|
101
|
+
f_start = 1 # Set chirp freq range min (Hz)
|
|
102
|
+
f_end = 20 # Set chirp freq range max (Hz)
|
|
103
|
+
data = chirp(t, f_start, t[-1], f_end, 'logarithmic')
|
|
104
|
+
# Compute the multitaper spectrogram
|
|
105
|
+
spect, stimes, sfreqs = spectrogram(data, fs, frequency_range, time_bandwidth, num_tapers,
|
|
106
|
+
window_params, min_nfft, detrend_opt, multiprocess,
|
|
107
|
+
cpus, weighting, plot_on, return_fig, clim_scale,
|
|
108
|
+
verbose, xyflip):
|
|
109
|
+
|
|
110
|
+
This code is companion to the paper:
|
|
111
|
+
"Sleep Neurophysiological Dynamics Through the Lens of Multitaper Spectral Analysis"
|
|
112
|
+
Michael J. Prerau, Ritchie E. Brown, Matt T. Bianchi, Jeffrey M. Ellenbogen, Patrick L. Purdon
|
|
113
|
+
December 7, 2016 : 60-92
|
|
114
|
+
DOI: 10.1152/physiol.00062.2015
|
|
115
|
+
which should be cited for academic use of this code.
|
|
116
|
+
|
|
117
|
+
A full tutorial on the multitaper spectrogram can be found at: # https://www.sleepEEG.org/multitaper
|
|
118
|
+
|
|
119
|
+
Copyright 2021 Michael J. Prerau Laboratory. - https://www.sleepEEG.org
|
|
120
|
+
Authors: Michael J. Prerau, Ph.D., Thomas Possidente, Mingjian He
|
|
121
|
+
|
|
122
|
+
__________________________________________________________________________________________________________________
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Process user input
|
|
126
|
+
[
|
|
127
|
+
data,
|
|
128
|
+
fs,
|
|
129
|
+
frequency_range,
|
|
130
|
+
time_bandwidth,
|
|
131
|
+
num_tapers,
|
|
132
|
+
winsize_samples,
|
|
133
|
+
winstep_samples,
|
|
134
|
+
window_start,
|
|
135
|
+
num_windows,
|
|
136
|
+
nfft,
|
|
137
|
+
detrend_opt,
|
|
138
|
+
plot_on,
|
|
139
|
+
verbose,
|
|
140
|
+
] = process_input(
|
|
141
|
+
data,
|
|
142
|
+
fs,
|
|
143
|
+
frequency_range,
|
|
144
|
+
time_bandwidth,
|
|
145
|
+
num_tapers,
|
|
146
|
+
window_params,
|
|
147
|
+
min_nfft,
|
|
148
|
+
detrend_opt,
|
|
149
|
+
plot_on,
|
|
150
|
+
verbose,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Set up spectrogram parameters
|
|
154
|
+
[window_idxs, stimes, sfreqs, freq_inds] = process_spectrogram_params(
|
|
155
|
+
fs, nfft, frequency_range, window_start, winsize_samples
|
|
156
|
+
)
|
|
157
|
+
# Display spectrogram parameters
|
|
158
|
+
if verbose:
|
|
159
|
+
display_spectrogram_props(
|
|
160
|
+
fs,
|
|
161
|
+
time_bandwidth,
|
|
162
|
+
num_tapers,
|
|
163
|
+
[winsize_samples, winstep_samples],
|
|
164
|
+
frequency_range,
|
|
165
|
+
nfft,
|
|
166
|
+
detrend_opt,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Split data into segments and preallocate
|
|
170
|
+
data_segments = data[window_idxs]
|
|
171
|
+
|
|
172
|
+
# COMPUTE THE MULTITAPER SPECTROGRAM
|
|
173
|
+
# STEP 1: Compute DPSS tapers based on desired spectral properties
|
|
174
|
+
# STEP 2: Multiply the data segment by the DPSS Tapers
|
|
175
|
+
# STEP 3: Compute the spectrum for each tapered segment
|
|
176
|
+
# STEP 4: Take the mean of the tapered spectra
|
|
177
|
+
|
|
178
|
+
# Compute DPSS tapers (STEP 1)
|
|
179
|
+
dpss_tapers, dpss_eigen = dpss(
|
|
180
|
+
winsize_samples, time_bandwidth, num_tapers, return_ratios=True
|
|
181
|
+
)
|
|
182
|
+
dpss_eigen = np.reshape(dpss_eigen, (num_tapers, 1))
|
|
183
|
+
|
|
184
|
+
# pre-compute weights
|
|
185
|
+
if weighting == "eigen":
|
|
186
|
+
wt = dpss_eigen / num_tapers
|
|
187
|
+
elif weighting == "unity":
|
|
188
|
+
wt = np.ones(num_tapers) / num_tapers
|
|
189
|
+
wt = np.reshape(wt, (num_tapers, 1)) # reshape as column vector
|
|
190
|
+
else:
|
|
191
|
+
wt = 0
|
|
192
|
+
|
|
193
|
+
tic = timeit.default_timer() # start timer
|
|
194
|
+
|
|
195
|
+
# Set up calc_mts_segment() input arguments
|
|
196
|
+
mts_params = (
|
|
197
|
+
dpss_tapers,
|
|
198
|
+
nfft,
|
|
199
|
+
freq_inds,
|
|
200
|
+
detrend_opt,
|
|
201
|
+
num_tapers,
|
|
202
|
+
dpss_eigen,
|
|
203
|
+
weighting,
|
|
204
|
+
wt,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if multiprocess: # use multiprocessing
|
|
208
|
+
n_jobs = max(cpu_count() - 1, 1) if n_jobs is None else n_jobs
|
|
209
|
+
mt_spectrogram = np.vstack(
|
|
210
|
+
Parallel(n_jobs=n_jobs)(
|
|
211
|
+
delayed(calc_mts_segment)(data_segments[num_window, :], *mts_params)
|
|
212
|
+
for num_window in range(num_windows)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
else: # if no multiprocessing, compute normally
|
|
217
|
+
mt_spectrogram = np.apply_along_axis(
|
|
218
|
+
calc_mts_segment, 1, data_segments, *mts_params
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Compute one-sided PSD spectrum
|
|
222
|
+
mt_spectrogram = mt_spectrogram.T
|
|
223
|
+
dc_select = np.where(sfreqs == 0)[0]
|
|
224
|
+
nyquist_select = np.where(sfreqs == fs / 2)[0]
|
|
225
|
+
select = np.setdiff1d(
|
|
226
|
+
np.arange(0, len(sfreqs)), np.concatenate((dc_select, nyquist_select))
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
mt_spectrogram = (
|
|
230
|
+
np.vstack(
|
|
231
|
+
[
|
|
232
|
+
mt_spectrogram[dc_select, :],
|
|
233
|
+
2 * mt_spectrogram[select, :],
|
|
234
|
+
mt_spectrogram[nyquist_select, :],
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
/ fs
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Flip if requested
|
|
241
|
+
if xyflip:
|
|
242
|
+
mt_spectrogram = mt_spectrogram.T
|
|
243
|
+
|
|
244
|
+
# End timer and get elapsed compute time
|
|
245
|
+
toc = timeit.default_timer()
|
|
246
|
+
if verbose:
|
|
247
|
+
print("\n Multitaper compute time: " + "%.2f" % (toc - tic) + " seconds")
|
|
248
|
+
|
|
249
|
+
if np.all(mt_spectrogram.flatten() == 0):
|
|
250
|
+
print("\n Data was all zeros, no output")
|
|
251
|
+
|
|
252
|
+
# # Plot multitaper spectrogram
|
|
253
|
+
# if plot_on:
|
|
254
|
+
# # convert from power to dB
|
|
255
|
+
# spect_data = nanpow2db(mt_spectrogram)
|
|
256
|
+
#
|
|
257
|
+
# # Set x and y axes
|
|
258
|
+
# dx = stimes[1] - stimes[0]
|
|
259
|
+
# dy = sfreqs[1] - sfreqs[0]
|
|
260
|
+
# extent = [stimes[0] - dx, stimes[-1] + dx, sfreqs[-1] + dy, sfreqs[0] - dy]
|
|
261
|
+
#
|
|
262
|
+
# # Plot spectrogram
|
|
263
|
+
# if ax is None:
|
|
264
|
+
# fig, ax = plt.subplots()
|
|
265
|
+
# else:
|
|
266
|
+
# fig = ax.get_figure()
|
|
267
|
+
# im = ax.imshow(spect_data, extent=extent, aspect="auto")
|
|
268
|
+
# fig.colorbar(im, ax=ax, label="PSD (dB)", shrink=0.8)
|
|
269
|
+
# ax.set_xlabel("Time (HH:MM:SS)")
|
|
270
|
+
# ax.set_ylabel("Frequency (Hz)")
|
|
271
|
+
# im.set_cmap(plt.cm.get_cmap("cet_rainbow4"))
|
|
272
|
+
# ax.invert_yaxis()
|
|
273
|
+
#
|
|
274
|
+
# # Scale colormap
|
|
275
|
+
# if clim_scale:
|
|
276
|
+
# clim = np.percentile(spect_data, [5, 98]) # from 5th percentile to 98th
|
|
277
|
+
# im.set_clim(clim) # actually change colorbar scale
|
|
278
|
+
#
|
|
279
|
+
# fig.show()
|
|
280
|
+
# if return_fig:
|
|
281
|
+
# return mt_spectrogram, stimes, sfreqs, (fig, ax)
|
|
282
|
+
|
|
283
|
+
return mt_spectrogram, stimes, sfreqs
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Helper Functions #
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Process User Inputs #
|
|
290
|
+
def process_input(
|
|
291
|
+
data,
|
|
292
|
+
fs,
|
|
293
|
+
frequency_range=None,
|
|
294
|
+
time_bandwidth=5,
|
|
295
|
+
num_tapers=None,
|
|
296
|
+
window_params=None,
|
|
297
|
+
min_nfft=0,
|
|
298
|
+
detrend_opt="linear",
|
|
299
|
+
plot_on=True,
|
|
300
|
+
verbose=True,
|
|
301
|
+
):
|
|
302
|
+
"""Helper function to process spectrogram() arguments
|
|
303
|
+
Arguments:
|
|
304
|
+
data (1d np.array): time series data-- required
|
|
305
|
+
fs (float): sampling frequency in Hz -- required
|
|
306
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] (default: [0 nyquist])
|
|
307
|
+
time_bandwidth (float): time-half bandwidth product (window duration*half bandwidth of main lobe)
|
|
308
|
+
(default: 5 Hz*s)
|
|
309
|
+
num_tapers (int): number of DPSS tapers to use (default: None [will be computed
|
|
310
|
+
as floor(2*time_bandwidth - 1)])
|
|
311
|
+
window_params (list): 1x2 list - [window size (seconds), step size (seconds)] (default: [5 1])
|
|
312
|
+
min_nfft (int): minimum allowable NFFT size, adds zero padding for interpolation (closest 2^x)
|
|
313
|
+
(default: 0)
|
|
314
|
+
detrend_opt (string): detrend data window ('linear' (default), 'constant', 'off')
|
|
315
|
+
(Default: 'linear')
|
|
316
|
+
plot_on (True): plot results (default: True)
|
|
317
|
+
verbose (True): display spectrogram properties (default: true)
|
|
318
|
+
Returns:
|
|
319
|
+
data (1d np.array): same as input
|
|
320
|
+
fs (float): same as input
|
|
321
|
+
frequency_range (list): same as input or calculated from fs if not given
|
|
322
|
+
time_bandwidth (float): same as input or default if not given
|
|
323
|
+
num_tapers (int): same as input or calculated from time_bandwidth if not given
|
|
324
|
+
winsize_samples (int): number of samples in single time window
|
|
325
|
+
winstep_samples (int): number of samples in a single window step
|
|
326
|
+
window_start (1xm np.array): array of timestamps representing the beginning time for each window
|
|
327
|
+
num_windows (int): number of windows in the data
|
|
328
|
+
nfft (int): length of signal to calculate fft on
|
|
329
|
+
detrend_opt ('string'): same as input or default if not given
|
|
330
|
+
plot_on (bool): same as input
|
|
331
|
+
verbose (bool): same as input
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
# Make sure data is 1 dimensional np array
|
|
335
|
+
if len(data.shape) != 1:
|
|
336
|
+
if (len(data.shape) == 2) & (
|
|
337
|
+
data.shape[1] == 1
|
|
338
|
+
): # if it's 2d, but can be transferred to 1d, do so
|
|
339
|
+
data = np.ravel(data[:, 0])
|
|
340
|
+
elif (len(data.shape) == 2) & (
|
|
341
|
+
data.shape[0] == 1
|
|
342
|
+
): # if it's 2d, but can be transferred to 1d, do so
|
|
343
|
+
data = np.ravel(data.T[:, 0])
|
|
344
|
+
else:
|
|
345
|
+
raise TypeError(
|
|
346
|
+
"Input data is the incorrect dimensions. Should be a 1d array with shape (n,) where n is \
|
|
347
|
+
the number of data points. Instead data shape was "
|
|
348
|
+
+ str(data.shape)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Set frequency range if not provided
|
|
352
|
+
if frequency_range is None:
|
|
353
|
+
frequency_range = [0, fs / 2]
|
|
354
|
+
|
|
355
|
+
# Set detrending method
|
|
356
|
+
detrend_opt = detrend_opt.lower()
|
|
357
|
+
if detrend_opt != "linear":
|
|
358
|
+
if detrend_opt in ["const", "constant"]:
|
|
359
|
+
detrend_opt = "constant"
|
|
360
|
+
elif detrend_opt in ["none", "false", "off"]:
|
|
361
|
+
detrend_opt = "off"
|
|
362
|
+
else:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"'"
|
|
365
|
+
+ str(detrend_opt)
|
|
366
|
+
+ "' is not a valid argument for detrend_opt. The choices "
|
|
367
|
+
+ "are: 'constant', 'linear', or 'off'."
|
|
368
|
+
)
|
|
369
|
+
# Check if frequency range is valid
|
|
370
|
+
if frequency_range[1] > fs / 2:
|
|
371
|
+
frequency_range[1] = fs / 2
|
|
372
|
+
warnings.warn(
|
|
373
|
+
"Upper frequency range greater than Nyquist, setting range to ["
|
|
374
|
+
+ str(frequency_range[0])
|
|
375
|
+
+ ", "
|
|
376
|
+
+ str(frequency_range[1])
|
|
377
|
+
+ "]"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Set number of tapers if none provided
|
|
381
|
+
if num_tapers is None:
|
|
382
|
+
num_tapers = math.floor(2 * time_bandwidth) - 1
|
|
383
|
+
|
|
384
|
+
# Warn if number of tapers is suboptimal
|
|
385
|
+
if num_tapers != math.floor(2 * time_bandwidth) - 1:
|
|
386
|
+
warnings.warn(
|
|
387
|
+
"Number of tapers is optimal at floor(2*TW) - 1. consider using "
|
|
388
|
+
+ str(math.floor(2 * time_bandwidth) - 1)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# If no window params provided, set to defaults
|
|
392
|
+
if window_params is None:
|
|
393
|
+
window_params = [5, 1]
|
|
394
|
+
|
|
395
|
+
# Check if window size is valid, fix if not
|
|
396
|
+
if window_params[0] * fs % 1 != 0:
|
|
397
|
+
winsize_samples = round(window_params[0] * fs)
|
|
398
|
+
warnings.warn(
|
|
399
|
+
"Window size is not divisible by sampling frequency. Adjusting window size to "
|
|
400
|
+
+ str(winsize_samples / fs)
|
|
401
|
+
+ " seconds"
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
winsize_samples = window_params[0] * fs
|
|
405
|
+
|
|
406
|
+
# Check if window step is valid, fix if not
|
|
407
|
+
if window_params[1] * fs % 1 != 0:
|
|
408
|
+
winstep_samples = round(window_params[1] * fs)
|
|
409
|
+
warnings.warn(
|
|
410
|
+
"Window step size is not divisible by sampling frequency. Adjusting window step size to "
|
|
411
|
+
+ str(winstep_samples / fs)
|
|
412
|
+
+ " seconds"
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
winstep_samples = window_params[1] * fs
|
|
416
|
+
|
|
417
|
+
# Get total data length
|
|
418
|
+
len_data = len(data)
|
|
419
|
+
|
|
420
|
+
# Check if length of data is smaller than window (bad)
|
|
421
|
+
if len_data < winsize_samples:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"\nData length ("
|
|
424
|
+
+ str(len_data)
|
|
425
|
+
+ ") is shorter than window size ("
|
|
426
|
+
+ str(winsize_samples)
|
|
427
|
+
+ "). Either increase data length or decrease window size."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Find window start indices and num of windows
|
|
431
|
+
window_start = np.arange(0, len_data - winsize_samples + 1, winstep_samples)
|
|
432
|
+
num_windows = len(window_start)
|
|
433
|
+
|
|
434
|
+
# Get num points in FFT
|
|
435
|
+
if min_nfft == 0: # avoid divide by zero error in np.log2(0)
|
|
436
|
+
nfft = max(2 ** math.ceil(np.log2(abs(winsize_samples))), winsize_samples)
|
|
437
|
+
else:
|
|
438
|
+
nfft = max(
|
|
439
|
+
max(2 ** math.ceil(np.log2(abs(winsize_samples))), winsize_samples),
|
|
440
|
+
2 ** math.ceil(np.log2(abs(min_nfft))),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
return [
|
|
444
|
+
data,
|
|
445
|
+
fs,
|
|
446
|
+
frequency_range,
|
|
447
|
+
time_bandwidth,
|
|
448
|
+
num_tapers,
|
|
449
|
+
int(winsize_samples),
|
|
450
|
+
int(winstep_samples),
|
|
451
|
+
window_start,
|
|
452
|
+
num_windows,
|
|
453
|
+
nfft,
|
|
454
|
+
detrend_opt,
|
|
455
|
+
plot_on,
|
|
456
|
+
verbose,
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# PROCESS THE SPECTROGRAM PARAMETERS #
|
|
461
|
+
def process_spectrogram_params(fs, nfft, frequency_range, window_start, datawin_size):
|
|
462
|
+
"""Helper function to create frequency vector and window indices
|
|
463
|
+
Arguments:
|
|
464
|
+
fs (float): sampling frequency in Hz -- required
|
|
465
|
+
nfft (int): length of signal to calculate fft on -- required
|
|
466
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] -- required
|
|
467
|
+
window_start (1xm np array): array of timestamps representing the beginning time for each
|
|
468
|
+
window -- required
|
|
469
|
+
datawin_size (float): seconds in one window -- required
|
|
470
|
+
Returns:
|
|
471
|
+
window_idxs (nxm np array): indices of timestamps for each window
|
|
472
|
+
(nxm where n=number of windows and m=datawin_size)
|
|
473
|
+
stimes (1xt np array): array of times for the center of the spectral bins
|
|
474
|
+
sfreqs (1xf np array): array of frequency bins for the spectrogram
|
|
475
|
+
freq_inds (1d np array): boolean array of which frequencies are being analyzed in
|
|
476
|
+
an array of frequencies from 0 to fs with steps of fs/nfft
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
# create frequency vector
|
|
480
|
+
df = fs / nfft
|
|
481
|
+
sfreqs = np.arange(0, fs, df)
|
|
482
|
+
|
|
483
|
+
# Get frequencies for given frequency range
|
|
484
|
+
freq_inds = (sfreqs >= frequency_range[0]) & (sfreqs <= frequency_range[1])
|
|
485
|
+
sfreqs = sfreqs[freq_inds]
|
|
486
|
+
|
|
487
|
+
# Compute times in the middle of each spectrum
|
|
488
|
+
window_middle_samples = window_start + round(datawin_size / 2)
|
|
489
|
+
stimes = window_middle_samples / fs
|
|
490
|
+
|
|
491
|
+
# Get indexes for each window
|
|
492
|
+
window_idxs = np.atleast_2d(window_start).T + np.arange(0, datawin_size, 1)
|
|
493
|
+
window_idxs = window_idxs.astype(int)
|
|
494
|
+
|
|
495
|
+
return [window_idxs, stimes, sfreqs, freq_inds]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# DISPLAY SPECTROGRAM PROPERTIES
|
|
499
|
+
def display_spectrogram_props(
|
|
500
|
+
fs,
|
|
501
|
+
time_bandwidth,
|
|
502
|
+
num_tapers,
|
|
503
|
+
data_window_params,
|
|
504
|
+
frequency_range,
|
|
505
|
+
nfft,
|
|
506
|
+
detrend_opt,
|
|
507
|
+
):
|
|
508
|
+
"""Prints spectrogram properties
|
|
509
|
+
Arguments:
|
|
510
|
+
fs (float): sampling frequency in Hz -- required
|
|
511
|
+
time_bandwidth (float): time-half bandwidth product (window duration*1/2*frequency_resolution) -- required
|
|
512
|
+
num_tapers (int): number of DPSS tapers to use -- required
|
|
513
|
+
data_window_params (list): 1x2 list - [window length(s), window step size(s)] -- required
|
|
514
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] -- required
|
|
515
|
+
nfft(float): number of fast fourier transform samples -- required
|
|
516
|
+
detrend_opt (str): detrend data window ('linear' (default), 'constant', 'off') -- required
|
|
517
|
+
Returns:
|
|
518
|
+
This function does not return anything
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
data_window_params = np.asarray(data_window_params) / fs
|
|
522
|
+
|
|
523
|
+
# Print spectrogram properties
|
|
524
|
+
print("Multitaper Spectrogram Properties: ")
|
|
525
|
+
print(
|
|
526
|
+
" Spectral Resolution: "
|
|
527
|
+
+ str(2 * time_bandwidth / data_window_params[0])
|
|
528
|
+
+ "Hz"
|
|
529
|
+
)
|
|
530
|
+
print(" Window Length: " + str(data_window_params[0]) + "s")
|
|
531
|
+
print(" Window Step: " + str(data_window_params[1]) + "s")
|
|
532
|
+
print(" Time Half-Bandwidth Product: " + str(time_bandwidth))
|
|
533
|
+
print(" Number of Tapers: " + str(num_tapers))
|
|
534
|
+
print(
|
|
535
|
+
" Frequency Range: "
|
|
536
|
+
+ str(frequency_range[0])
|
|
537
|
+
+ "-"
|
|
538
|
+
+ str(frequency_range[1])
|
|
539
|
+
+ "Hz"
|
|
540
|
+
)
|
|
541
|
+
print(" NFFT: " + str(nfft))
|
|
542
|
+
print(" Detrend: " + detrend_opt + "\n")
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# NANPOW2DB
|
|
546
|
+
def nanpow2db(y):
|
|
547
|
+
"""Power to dB conversion, setting bad values to nans
|
|
548
|
+
Arguments:
|
|
549
|
+
y (float or array-like): power
|
|
550
|
+
Returns:
|
|
551
|
+
ydB (float or np array): inputs converted to dB with 0s and negatives resulting in nans
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
if isinstance(y, int) or isinstance(y, float):
|
|
555
|
+
if y == 0:
|
|
556
|
+
return np.nan
|
|
557
|
+
else:
|
|
558
|
+
ydB = 10 * np.log10(y)
|
|
559
|
+
else:
|
|
560
|
+
if isinstance(y, list): # if list, turn into array
|
|
561
|
+
y = np.asarray(y)
|
|
562
|
+
y = y.astype(float) # make sure it's a float array so we can put nans in it
|
|
563
|
+
y[y == 0] = np.nan
|
|
564
|
+
ydB = 10 * np.log10(y)
|
|
565
|
+
|
|
566
|
+
return ydB
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# Helper #
|
|
570
|
+
def is_outlier(data):
|
|
571
|
+
smad = 1.4826 * np.median(
|
|
572
|
+
abs(data - np.median(data))
|
|
573
|
+
) # scaled median absolute deviation
|
|
574
|
+
outlier_mask = (
|
|
575
|
+
abs(data - np.median(data)) > 3 * smad
|
|
576
|
+
) # outliers are more than 3 smads away from median
|
|
577
|
+
outlier_mask = outlier_mask | np.isnan(data) | np.isinf(data)
|
|
578
|
+
return outlier_mask
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# CALCULATE MULTITAPER SPECTRUM ON SINGLE SEGMENT
|
|
582
|
+
def calc_mts_segment(
|
|
583
|
+
data_segment,
|
|
584
|
+
dpss_tapers,
|
|
585
|
+
nfft,
|
|
586
|
+
freq_inds,
|
|
587
|
+
detrend_opt,
|
|
588
|
+
num_tapers,
|
|
589
|
+
dpss_eigen,
|
|
590
|
+
weighting,
|
|
591
|
+
wt,
|
|
592
|
+
):
|
|
593
|
+
"""Helper function to calculate the multitaper spectrum of a single segment of data
|
|
594
|
+
Arguments:
|
|
595
|
+
data_segment (1d np.array): One window worth of time-series data -- required
|
|
596
|
+
dpss_tapers (2d np.array): Parameters for the DPSS tapers to be used.
|
|
597
|
+
Dimensions are (num_tapers, winsize_samples) -- required
|
|
598
|
+
nfft (int): length of signal to calculate fft on -- required
|
|
599
|
+
freq_inds (1d np array): boolean array of which frequencies are being analyzed in
|
|
600
|
+
an array of frequencies from 0 to fs with steps of fs/nfft
|
|
601
|
+
detrend_opt (str): detrend data window ('linear' (default), 'constant', 'off')
|
|
602
|
+
num_tapers (int): number of tapers being used
|
|
603
|
+
dpss_eigen (np array):
|
|
604
|
+
weighting (str):
|
|
605
|
+
wt (int or np array):
|
|
606
|
+
Returns:
|
|
607
|
+
mt_spectrum (1d np.array): spectral power for single window
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
# If segment has all zeros, return vector of zeros
|
|
611
|
+
if all(data_segment == 0):
|
|
612
|
+
ret = np.empty(sum(freq_inds))
|
|
613
|
+
ret.fill(0)
|
|
614
|
+
return ret
|
|
615
|
+
|
|
616
|
+
if any(np.isnan(data_segment)):
|
|
617
|
+
ret = np.empty(sum(freq_inds))
|
|
618
|
+
ret.fill(np.nan)
|
|
619
|
+
return ret
|
|
620
|
+
|
|
621
|
+
# Option to detrend data to remove low frequency DC component
|
|
622
|
+
if detrend_opt != "off":
|
|
623
|
+
data_segment = detrend(data_segment, type=detrend_opt)
|
|
624
|
+
|
|
625
|
+
# Multiply data by dpss tapers (STEP 2)
|
|
626
|
+
tapered_data = np.multiply(np.asmatrix(data_segment).T, np.asmatrix(dpss_tapers.T))
|
|
627
|
+
|
|
628
|
+
# Compute the FFT (STEP 3)
|
|
629
|
+
fft_data = np.fft.fft(tapered_data, nfft, axis=0)
|
|
630
|
+
|
|
631
|
+
# Compute the weighted mean spectral power across tapers (STEP 4)
|
|
632
|
+
spower = np.power(np.imag(fft_data), 2) + np.power(np.real(fft_data), 2)
|
|
633
|
+
if weighting == "adapt":
|
|
634
|
+
# adaptive weights - for colored noise spectrum (Percival & Walden p368-370)
|
|
635
|
+
tpower = np.dot(np.transpose(data_segment), (data_segment / len(data_segment)))
|
|
636
|
+
spower_iter = np.mean(spower[:, 0:2], 1)
|
|
637
|
+
spower_iter = spower_iter[:, np.newaxis]
|
|
638
|
+
a = (1 - dpss_eigen) * tpower
|
|
639
|
+
for i in range(3): # 3 iterations only
|
|
640
|
+
# Calc the MSE weights
|
|
641
|
+
b = np.dot(spower_iter, np.ones((1, num_tapers))) / (
|
|
642
|
+
(np.dot(spower_iter, np.transpose(dpss_eigen)))
|
|
643
|
+
+ (np.ones((nfft, 1)) * np.transpose(a))
|
|
644
|
+
)
|
|
645
|
+
# Calc new spectral estimate
|
|
646
|
+
wk = (b**2) * np.dot(np.ones((nfft, 1)), np.transpose(dpss_eigen))
|
|
647
|
+
spower_iter = np.sum((np.transpose(wk) * np.transpose(spower)), 0) / np.sum(
|
|
648
|
+
wk, 1
|
|
649
|
+
)
|
|
650
|
+
spower_iter = spower_iter[:, np.newaxis]
|
|
651
|
+
|
|
652
|
+
mt_spectrum = np.squeeze(spower_iter)
|
|
653
|
+
|
|
654
|
+
else:
|
|
655
|
+
# eigenvalue or uniform weights
|
|
656
|
+
mt_spectrum = np.dot(spower, wt)
|
|
657
|
+
mt_spectrum = np.reshape(mt_spectrum, nfft) # reshape to 1D
|
|
658
|
+
|
|
659
|
+
return mt_spectrum[freq_inds]
|