py2ls 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.
- py2ls/.git/COMMIT_EDITMSG +1 -0
- py2ls/.git/FETCH_HEAD +1 -0
- py2ls/.git/HEAD +1 -0
- py2ls/.git/config +15 -0
- py2ls/.git/description +1 -0
- py2ls/.git/hooks/applypatch-msg.sample +15 -0
- py2ls/.git/hooks/commit-msg.sample +24 -0
- py2ls/.git/hooks/fsmonitor-watchman.sample +174 -0
- py2ls/.git/hooks/post-update.sample +8 -0
- py2ls/.git/hooks/pre-applypatch.sample +14 -0
- py2ls/.git/hooks/pre-commit.sample +49 -0
- py2ls/.git/hooks/pre-merge-commit.sample +13 -0
- py2ls/.git/hooks/pre-push.sample +53 -0
- py2ls/.git/hooks/pre-rebase.sample +169 -0
- py2ls/.git/hooks/pre-receive.sample +24 -0
- py2ls/.git/hooks/prepare-commit-msg.sample +42 -0
- py2ls/.git/hooks/push-to-checkout.sample +78 -0
- py2ls/.git/hooks/update.sample +128 -0
- py2ls/.git/index +0 -0
- py2ls/.git/info/exclude +6 -0
- py2ls/.git/logs/HEAD +1 -0
- py2ls/.git/logs/refs/heads/main +1 -0
- py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
- py2ls/.git/logs/refs/remotes/origin/main +1 -0
- py2ls/.git/objects/25/b796accd261b9135fd32a2c00785f68edf6c46 +0 -0
- py2ls/.git/objects/36/b4a1b7403abc6c360f8fe2cb656ab945254971 +0 -0
- py2ls/.git/objects/3f/d6561300938afbb3d11976cf9c8f29549280d9 +0 -0
- py2ls/.git/objects/58/20a729045d4dc7e37ccaf8aa8eec126850afe2 +0 -0
- py2ls/.git/objects/60/f273eb1c412d916fa3f11318a7da7a9911b52a +0 -0
- py2ls/.git/objects/61/570cec8c061abe74121f27f5face6c69b98f99 +0 -0
- py2ls/.git/objects/69/13c452ca319f7cbf6a0836dc10a5bb033c84e4 +0 -0
- py2ls/.git/objects/78/3d4167bc95c9d2175e0df03ef1c1c880ba75ab +0 -0
- py2ls/.git/objects/79/7ae089b2212a937840e215276005ce76881307 +0 -0
- py2ls/.git/objects/7e/5956c806b5edc344d46dab599dec337891ba1f +1 -0
- py2ls/.git/objects/8e/55a7d2b96184030211f20c9b9af201eefcac82 +0 -0
- py2ls/.git/objects/91/c69ad88fe0ba94aa7859fb5f7edac5e6f1a3f7 +0 -0
- py2ls/.git/objects/b0/56be4be89ba6b76949dd641df45bb7036050c8 +0 -0
- py2ls/.git/objects/b0/9cd7856d58590578ee1a4f3ad45d1310a97f87 +0 -0
- py2ls/.git/objects/d9/005f2cc7fc4e65f14ed5518276007c08cf2fd0 +0 -0
- py2ls/.git/objects/df/e0770424b2a19faf507a501ebfc23be8f54e7b +0 -0
- py2ls/.git/objects/e9/391ffe371f1cc43b42ef09b705d9c767c2e14f +0 -0
- py2ls/.git/objects/fc/292e793ecfd42240ac43be407023bd731fa9e7 +0 -0
- py2ls/.git/refs/heads/main +1 -0
- py2ls/.git/refs/remotes/origin/HEAD +1 -0
- py2ls/.git/refs/remotes/origin/main +1 -0
- py2ls/.gitattributes +2 -0
- py2ls/.gitignore +152 -0
- py2ls/LICENSE +201 -0
- py2ls/README.md +409 -0
- py2ls/__init__.py +17 -0
- py2ls/brain_atlas.py +145 -0
- py2ls/correlators.py +475 -0
- py2ls/dbhandler.py +97 -0
- py2ls/freqanalysis.py +800 -0
- py2ls/internet_finder.py +405 -0
- py2ls/ips.py +2844 -0
- py2ls/netfinder.py +780 -0
- py2ls/sleep_events_detectors.py +1350 -0
- py2ls/translator.py +686 -0
- py2ls/version.py +1 -0
- py2ls/wb_detector.py +169 -0
- py2ls-0.1.0.dist-info/METADATA +12 -0
- py2ls-0.1.0.dist-info/RECORD +64 -0
- py2ls-0.1.0.dist-info/WHEEL +4 -0
py2ls/freqanalysis.py
ADDED
@@ -0,0 +1,800 @@
|
|
1
|
+
# Analysis Imports
|
2
|
+
import math
|
3
|
+
import numpy as np
|
4
|
+
from scipy.signal.windows import dpss
|
5
|
+
from scipy.signal import detrend
|
6
|
+
|
7
|
+
# Logistical Imports
|
8
|
+
import warnings
|
9
|
+
import timeit
|
10
|
+
from joblib import Parallel, delayed, cpu_count
|
11
|
+
|
12
|
+
# Visualization imports
|
13
|
+
# noinspection PyUnresolvedReferences
|
14
|
+
import colorcet # this import is necessary to add rainbow colormap to matplotlib
|
15
|
+
import matplotlib.pyplot as plt
|
16
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
17
|
+
|
18
|
+
|
19
|
+
# MULTITAPER SPECTROGRAM #
|
20
|
+
def multitaper_spectrogram(
|
21
|
+
data,
|
22
|
+
fs,
|
23
|
+
frequency_range=None,
|
24
|
+
time_bandwidth=5,
|
25
|
+
num_tapers=None,
|
26
|
+
window_params=None,
|
27
|
+
min_nfft=0,
|
28
|
+
detrend_opt="linear",
|
29
|
+
multiprocess=True,
|
30
|
+
n_jobs=4,
|
31
|
+
weighting="unity",
|
32
|
+
plot_on=False,
|
33
|
+
return_fig=False,
|
34
|
+
clim_scale=True,
|
35
|
+
verbose=True,
|
36
|
+
xyflip=False,
|
37
|
+
ax=None,
|
38
|
+
):
|
39
|
+
"""
|
40
|
+
Compute multitaper spectrogram of timeseries data
|
41
|
+
Usage:
|
42
|
+
mt_spectrogram, stimes, sfreqs = multitaper_spectrogram(data, fs, frequency_range=None, time_bandwidth=5,
|
43
|
+
num_tapers=None, window_params=None, min_nfft=0,
|
44
|
+
detrend_opt='linear', multiprocess=True, cpus=4,
|
45
|
+
weighting='unity', plot_on=True, return_fig=False,
|
46
|
+
clim_scale=True, verbose=True, xyflip=False):
|
47
|
+
Arguments:
|
48
|
+
data (1d np.array): time series data -- required
|
49
|
+
fs (float): sampling frequency in Hz -- required
|
50
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] (default: [0 nyquist])
|
51
|
+
time_bandwidth (float): time-half bandwidth product (window duration*half bandwidth of main lobe)
|
52
|
+
(default: 5 Hz*s)
|
53
|
+
# Set time-half bandwidth >1:
|
54
|
+
# The time-half bandwidth is a measure of the width of the wavelet in the
|
55
|
+
# time domain, specifically indicating the extent of the wavelet in time
|
56
|
+
# at which it decays to half of its maximum value. It characterizes the
|
57
|
+
# temporal localization of the wavelet. A smaller time-half bandwidth
|
58
|
+
# results in better temporal localization but poorer frequency resolution,
|
59
|
+
# while a larger time-half bandwidth provides better frequency resolution
|
60
|
+
# but poorer temporal localization.
|
61
|
+
# Time-Bandwidth Product (NW): controls the trade-off between frequency
|
62
|
+
# resolution and temporal resolution. Higher values of NW result in better
|
63
|
+
# frequency resolution but poorer temporal resolution, and vice versa. It
|
64
|
+
# is typically set based on the desired frequency resolution and the length
|
65
|
+
# of your data window.
|
66
|
+
num_tapers (int): number of DPSS tapers to use (default: [will be computed
|
67
|
+
as floor(2*time_bandwidth - 1)])
|
68
|
+
# Set number of tapers (optimal is time_bandwidth*2 - 1)
|
69
|
+
# In spectral analysis methods such as multitaper spectral estimation, the
|
70
|
+
# number of tapers is a parameter that determines the trade-off between
|
71
|
+
# frequency resolution and variance reduction. It does not directly relate
|
72
|
+
# to the time-half bandwidth but rather influences the spectral estimation
|
73
|
+
# process.
|
74
|
+
# The number of tapers refers to how many of these tapered windows are used in
|
75
|
+
# the spectral estimation process.
|
76
|
+
# Increasing the number of tapers improves frequency resolution and reduces
|
77
|
+
# variance but decreases the effective length of the data segment for each
|
78
|
+
# taper.
|
79
|
+
# Conversely, using fewer tapers decreases frequency resolution but increases
|
80
|
+
# the effective length of the data segment, which can be beneficial for capturing
|
81
|
+
# lower-frequency components.
|
82
|
+
# Number of Tapers (K): The number of tapers determines the number of orthogonal
|
83
|
+
# tapers used in the multitaper spectral estimation. Increasing the number of
|
84
|
+
# tapers improves frequency resolution but reduces the effective bandwidth of
|
85
|
+
# each taper. The number of tapers is related to the time-bandwidth product NW
|
86
|
+
# and is often chosen as 2NW - 1.
|
87
|
+
window_params (list): 1x2 list - [window size (seconds), step size (seconds)] (default: [5 1])
|
88
|
+
# Window size is 4s(first para, total sec) with step size of 1s (wnd para, 0.01 = 1 sec)
|
89
|
+
# [1, 0.01] # Window size is 4s(first para, total sec) with step size of 1s (wnd para, 0.01 = 1 sec)
|
90
|
+
# Length of the Window: The length of the window is related to the duration of
|
91
|
+
# the analysis window used for computing the spectral estimates. It should be
|
92
|
+
# chosen based on the characteristics of your data and the desired trade-off
|
93
|
+
# between frequency and temporal resolution.
|
94
|
+
min_nfft (int): minimum allowable NFFT size, adds zero padding for interpolation (closest 2^x)
|
95
|
+
(default: 0)
|
96
|
+
detrend_opt (string): detrend data window ('linear' (default), 'constant', 'off')
|
97
|
+
# detrend each window by subtracting the average
|
98
|
+
multiprocess (bool): Use multiprocessing to compute multitaper spectrogram (default: True)
|
99
|
+
n_jobs (int): Number of cpus to use if multiprocess = True (default: True). Note: if default is left
|
100
|
+
as None and multiprocess = True, the number of cpus used for multiprocessing will be
|
101
|
+
all available - 1.
|
102
|
+
weighting (str): weighting of tapers ('unity' (default), 'eigen', 'adapt');
|
103
|
+
# weight each taper at 1
|
104
|
+
plot_on (bool): plot results (default: True)
|
105
|
+
return_fig (bool): return plotted spectrogram (default: False)
|
106
|
+
clim_scale (bool): automatically scale the colormap on the plotted spectrogram (default: True)
|
107
|
+
verbose (bool): display spectrogram properties (default: True)
|
108
|
+
xyflip (bool): transpose the mt_spectrogram output (default: False)
|
109
|
+
ax (axes): a matplotlib axes to plot the spectrogram on (default: None)
|
110
|
+
Returns:
|
111
|
+
mt_spectrogram (TxF np array): spectral power matrix
|
112
|
+
stimes (1xT np array): timepoints (s) in mt_spectrogram
|
113
|
+
sfreqs (1xF np array)L frequency values (Hz) in mt_spectrogram
|
114
|
+
|
115
|
+
Example:
|
116
|
+
In this example we create some chirp data and run the multitaper spectrogram on it.
|
117
|
+
import numpy as np # import numpy
|
118
|
+
from scipy.signal import chirp # import chirp generation function
|
119
|
+
# Set spectrogram params
|
120
|
+
fs = 200 # Sampling Frequency
|
121
|
+
frequency_range = [0, 25] # Limit frequencies from 0 to 25 Hz
|
122
|
+
time_bandwidth = 3 # Set time-half bandwidth
|
123
|
+
num_tapers = 5 # Set number of tapers (optimal is time_bandwidth*2 - 1)
|
124
|
+
window_params = [4, 1] # Window size is 4s with step size of 1s
|
125
|
+
min_nfft = 0 # No minimum nfft
|
126
|
+
detrend_opt = 'constant' # detrend each window by subtracting the average
|
127
|
+
multiprocess = True # use multiprocessing
|
128
|
+
cpus = 4 # use 4 cores in multiprocessing
|
129
|
+
weighting = 'unity' # weight each taper at 1
|
130
|
+
plot_on = True # plot spectrogram
|
131
|
+
return_fig = False # do not return plotted spectrogram
|
132
|
+
clim_scale = False # don't auto-scale the colormap
|
133
|
+
verbose = True # print extra info
|
134
|
+
xyflip = False # do not transpose spect output matrix
|
135
|
+
|
136
|
+
# Generate sample chirp data
|
137
|
+
t = np.arange(1/fs, 600, 1/fs) # Create 10 min time array from 1/fs to 600 stepping by 1/fs
|
138
|
+
f_start = 1 # Set chirp freq range min (Hz)
|
139
|
+
f_end = 20 # Set chirp freq range max (Hz)
|
140
|
+
data = chirp(t, f_start, t[-1], f_end, 'logarithmic')
|
141
|
+
# Compute the multitaper spectrogram
|
142
|
+
spect, stimes, sfreqs = multitaper_spectrogram(data, fs, frequency_range, time_bandwidth, num_tapers,
|
143
|
+
window_params, min_nfft, detrend_opt, multiprocess,
|
144
|
+
cpus, weighting, plot_on, return_fig, clim_scale,
|
145
|
+
verbose, xyflip):
|
146
|
+
|
147
|
+
This code is companion to the paper:
|
148
|
+
"Sleep Neurophysiological Dynamics Through the Lens of Multitaper Spectral Analysis"
|
149
|
+
Michael J. Prerau, Ritchie E. Brown, Matt T. Bianchi, Jeffrey M. Ellenbogen, Patrick L. Purdon
|
150
|
+
December 7, 2016 : 60-92
|
151
|
+
DOI: 10.1152/physiol.00062.2015
|
152
|
+
which should be cited for academic use of this code.
|
153
|
+
|
154
|
+
A full tutorial on the multitaper spectrogram can be found at: # https://www.sleepEEG.org/multitaper
|
155
|
+
|
156
|
+
Copyright 2021 Michael J. Prerau Laboratory. - https://www.sleepEEG.org
|
157
|
+
Authors: Michael J. Prerau, Ph.D., Thomas Possidente, Mingjian He
|
158
|
+
|
159
|
+
__________________________________________________________________________________________________________________
|
160
|
+
"""
|
161
|
+
|
162
|
+
# Process user input
|
163
|
+
[
|
164
|
+
data,
|
165
|
+
fs,
|
166
|
+
frequency_range,
|
167
|
+
time_bandwidth,
|
168
|
+
num_tapers,
|
169
|
+
winsize_samples,
|
170
|
+
winstep_samples,
|
171
|
+
window_start,
|
172
|
+
num_windows,
|
173
|
+
nfft,
|
174
|
+
detrend_opt,
|
175
|
+
plot_on,
|
176
|
+
verbose,
|
177
|
+
] = process_input(
|
178
|
+
data,
|
179
|
+
fs,
|
180
|
+
frequency_range,
|
181
|
+
time_bandwidth,
|
182
|
+
num_tapers,
|
183
|
+
window_params,
|
184
|
+
min_nfft,
|
185
|
+
detrend_opt,
|
186
|
+
plot_on,
|
187
|
+
verbose,
|
188
|
+
)
|
189
|
+
|
190
|
+
# Set up spectrogram parameters
|
191
|
+
[window_idxs, stimes, sfreqs, freq_inds] = process_spectrogram_params(
|
192
|
+
fs, nfft, frequency_range, window_start, winsize_samples
|
193
|
+
)
|
194
|
+
# Display spectrogram parameters
|
195
|
+
if verbose:
|
196
|
+
display_spectrogram_props(
|
197
|
+
fs,
|
198
|
+
time_bandwidth,
|
199
|
+
num_tapers,
|
200
|
+
[winsize_samples, winstep_samples],
|
201
|
+
frequency_range,
|
202
|
+
nfft,
|
203
|
+
detrend_opt,
|
204
|
+
)
|
205
|
+
|
206
|
+
# Split data into segments and preallocate
|
207
|
+
data_segments = data[window_idxs]
|
208
|
+
|
209
|
+
# COMPUTE THE MULTITAPER SPECTROGRAM
|
210
|
+
# STEP 1: Compute DPSS tapers based on desired spectral properties
|
211
|
+
# STEP 2: Multiply the data segment by the DPSS Tapers
|
212
|
+
# STEP 3: Compute the spectrum for each tapered segment
|
213
|
+
# STEP 4: Take the mean of the tapered spectra
|
214
|
+
|
215
|
+
# Compute DPSS tapers (STEP 1)
|
216
|
+
dpss_tapers, dpss_eigen = dpss(
|
217
|
+
winsize_samples, time_bandwidth, num_tapers, return_ratios=True
|
218
|
+
)
|
219
|
+
dpss_eigen = np.reshape(dpss_eigen, (num_tapers, 1))
|
220
|
+
|
221
|
+
# pre-compute weights
|
222
|
+
if weighting == "eigen":
|
223
|
+
wt = dpss_eigen / num_tapers
|
224
|
+
elif weighting == "unity":
|
225
|
+
wt = np.ones(num_tapers) / num_tapers
|
226
|
+
wt = np.reshape(wt, (num_tapers, 1)) # reshape as column vector
|
227
|
+
else:
|
228
|
+
wt = 0
|
229
|
+
|
230
|
+
tic = timeit.default_timer() # start timer
|
231
|
+
|
232
|
+
# Set up calc_mts_segment() input arguments
|
233
|
+
mts_params = (
|
234
|
+
dpss_tapers,
|
235
|
+
nfft,
|
236
|
+
freq_inds,
|
237
|
+
detrend_opt,
|
238
|
+
num_tapers,
|
239
|
+
dpss_eigen,
|
240
|
+
weighting,
|
241
|
+
wt,
|
242
|
+
)
|
243
|
+
|
244
|
+
if multiprocess: # use multiprocessing
|
245
|
+
n_jobs = max(cpu_count() - 1, 1) if n_jobs is None else n_jobs
|
246
|
+
mt_spectrogram = np.vstack(
|
247
|
+
Parallel(n_jobs=n_jobs)(
|
248
|
+
delayed(calc_mts_segment)(data_segments[num_window, :], *mts_params)
|
249
|
+
for num_window in range(num_windows)
|
250
|
+
)
|
251
|
+
)
|
252
|
+
|
253
|
+
else: # if no multiprocessing, compute normally
|
254
|
+
mt_spectrogram = np.apply_along_axis(
|
255
|
+
calc_mts_segment, 1, data_segments, *mts_params
|
256
|
+
)
|
257
|
+
|
258
|
+
# Compute one-sided PSD spectrum
|
259
|
+
mt_spectrogram = mt_spectrogram.T
|
260
|
+
dc_select = np.where(sfreqs == 0)[0]
|
261
|
+
nyquist_select = np.where(sfreqs == fs / 2)[0]
|
262
|
+
select = np.setdiff1d(
|
263
|
+
np.arange(0, len(sfreqs)), np.concatenate((dc_select, nyquist_select))
|
264
|
+
)
|
265
|
+
|
266
|
+
mt_spectrogram = (
|
267
|
+
np.vstack(
|
268
|
+
[
|
269
|
+
mt_spectrogram[dc_select, :],
|
270
|
+
2 * mt_spectrogram[select, :],
|
271
|
+
mt_spectrogram[nyquist_select, :],
|
272
|
+
]
|
273
|
+
)
|
274
|
+
/ fs
|
275
|
+
)
|
276
|
+
|
277
|
+
# Flip if requested
|
278
|
+
if xyflip:
|
279
|
+
mt_spectrogram = mt_spectrogram.T
|
280
|
+
|
281
|
+
# End timer and get elapsed compute time
|
282
|
+
toc = timeit.default_timer()
|
283
|
+
if verbose:
|
284
|
+
print("\n Multitaper compute time: " + "%.2f" % (toc - tic) + " seconds")
|
285
|
+
|
286
|
+
if np.all(mt_spectrogram.flatten() == 0):
|
287
|
+
print("\n Data was all zeros, no output")
|
288
|
+
print(
|
289
|
+
f"\ntime.shape={stimes.shape} \nfreqs.shape={sfreqs.shape}\nspectrogram.shape={mt_spectrogram.shape}"
|
290
|
+
)
|
291
|
+
# Plot multitaper spectrogram
|
292
|
+
if plot_on:
|
293
|
+
# convert from power to dB
|
294
|
+
spect_data = nanpow2db(mt_spectrogram)
|
295
|
+
|
296
|
+
# Set x and y axes
|
297
|
+
dx = stimes[1] - stimes[0]
|
298
|
+
dy = sfreqs[1] - sfreqs[0]
|
299
|
+
extent = [stimes[0] - dx, stimes[-1] + dx, sfreqs[-1] + dy, sfreqs[0] - dy]
|
300
|
+
|
301
|
+
# Plot spectrogram
|
302
|
+
if ax is None:
|
303
|
+
fig, ax = plt.subplots()
|
304
|
+
else:
|
305
|
+
fig = ax.get_figure()
|
306
|
+
im = ax.imshow(spect_data, extent=extent, aspect="auto")
|
307
|
+
fig.colorbar(im, ax=ax, label="PSD (dB)", shrink=0.8)
|
308
|
+
ax.set_xlabel("Time (HH:MM:SS)")
|
309
|
+
ax.set_ylabel("Frequency (Hz)")
|
310
|
+
im.set_cmap(plt.cm.get_cmap("cet_rainbow4"))
|
311
|
+
ax.invert_yaxis()
|
312
|
+
|
313
|
+
# Scale colormap
|
314
|
+
if clim_scale:
|
315
|
+
clim = np.percentile(spect_data, [5, 98]) # from 5th percentile to 98th
|
316
|
+
im.set_clim(clim) # actually change colorbar scale
|
317
|
+
|
318
|
+
fig.show()
|
319
|
+
if return_fig:
|
320
|
+
return stimes, sfreqs, mt_spectrogram, (fig, ax)
|
321
|
+
else:
|
322
|
+
return stimes, sfreqs, mt_spectrogram
|
323
|
+
|
324
|
+
|
325
|
+
# Helper Functions #
|
326
|
+
|
327
|
+
|
328
|
+
# Process User Inputs #
|
329
|
+
def process_input(
|
330
|
+
data,
|
331
|
+
fs,
|
332
|
+
frequency_range=None,
|
333
|
+
time_bandwidth=5,
|
334
|
+
num_tapers=None,
|
335
|
+
window_params=None,
|
336
|
+
min_nfft=0,
|
337
|
+
detrend_opt="linear",
|
338
|
+
plot_on=False,
|
339
|
+
verbose=True,
|
340
|
+
):
|
341
|
+
"""Helper function to process multitaper_spectrogram() arguments
|
342
|
+
Arguments:
|
343
|
+
data (1d np.array): time series data-- required
|
344
|
+
fs (float): sampling frequency in Hz -- required
|
345
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] (default: [0 nyquist])
|
346
|
+
time_bandwidth (float): time-half bandwidth product (window duration*half bandwidth of main lobe)
|
347
|
+
(default: 5 Hz*s)
|
348
|
+
num_tapers (int): number of DPSS tapers to use (default: None [will be computed
|
349
|
+
as floor(2*time_bandwidth - 1)])
|
350
|
+
window_params (list): 1x2 list - [window size (seconds), step size (seconds)] (default: [5 1])
|
351
|
+
min_nfft (int): minimum allowable NFFT size, adds zero padding for interpolation (closest 2^x)
|
352
|
+
(default: 0)
|
353
|
+
detrend_opt (string): detrend data window ('linear' (default), 'constant', 'off')
|
354
|
+
(Default: 'linear')
|
355
|
+
plot_on (True): plot results (default: True)
|
356
|
+
verbose (True): display spectrogram properties (default: true)
|
357
|
+
Returns:
|
358
|
+
data (1d np.array): same as input
|
359
|
+
fs (float): same as input
|
360
|
+
frequency_range (list): same as input or calculated from fs if not given
|
361
|
+
time_bandwidth (float): same as input or default if not given
|
362
|
+
num_tapers (int): same as input or calculated from time_bandwidth if not given
|
363
|
+
winsize_samples (int): number of samples in single time window
|
364
|
+
winstep_samples (int): number of samples in a single window step
|
365
|
+
window_start (1xm np.array): array of timestamps representing the beginning time for each window
|
366
|
+
num_windows (int): number of windows in the data
|
367
|
+
nfft (int): length of signal to calculate fft on
|
368
|
+
detrend_opt ('string'): same as input or default if not given
|
369
|
+
plot_on (bool): same as input
|
370
|
+
verbose (bool): same as input
|
371
|
+
"""
|
372
|
+
|
373
|
+
# Make sure data is 1 dimensional np array
|
374
|
+
if len(data.shape) != 1:
|
375
|
+
if (len(data.shape) == 2) & (
|
376
|
+
data.shape[1] == 1
|
377
|
+
): # if it's 2d, but can be transferred to 1d, do so
|
378
|
+
data = np.ravel(data[:, 0])
|
379
|
+
elif (len(data.shape) == 2) & (
|
380
|
+
data.shape[0] == 1
|
381
|
+
): # if it's 2d, but can be transferred to 1d, do so
|
382
|
+
data = np.ravel(data.T[:, 0])
|
383
|
+
else:
|
384
|
+
raise TypeError(
|
385
|
+
"Input data is the incorrect dimensions. Should be a 1d array with shape (n,) where n is \
|
386
|
+
the number of data points. Instead data shape was "
|
387
|
+
+ str(data.shape)
|
388
|
+
)
|
389
|
+
|
390
|
+
# Set frequency range if not provided
|
391
|
+
if frequency_range is None:
|
392
|
+
frequency_range = [0, fs / 2]
|
393
|
+
|
394
|
+
# Set detrending method
|
395
|
+
detrend_opt = detrend_opt.lower()
|
396
|
+
if detrend_opt != "linear":
|
397
|
+
if detrend_opt in ["const", "constant"]:
|
398
|
+
detrend_opt = "constant"
|
399
|
+
elif detrend_opt in ["none", "false", "off"]:
|
400
|
+
detrend_opt = "off"
|
401
|
+
else:
|
402
|
+
raise ValueError(
|
403
|
+
"'"
|
404
|
+
+ str(detrend_opt)
|
405
|
+
+ "' is not a valid argument for detrend_opt. The choices "
|
406
|
+
+ "are: 'constant', 'linear', or 'off'."
|
407
|
+
)
|
408
|
+
# Check if frequency range is valid
|
409
|
+
if frequency_range[1] > fs / 2:
|
410
|
+
frequency_range[1] = fs / 2
|
411
|
+
warnings.warn(
|
412
|
+
"Upper frequency range greater than Nyquist, setting range to ["
|
413
|
+
+ str(frequency_range[0])
|
414
|
+
+ ", "
|
415
|
+
+ str(frequency_range[1])
|
416
|
+
+ "]"
|
417
|
+
)
|
418
|
+
|
419
|
+
# Set number of tapers if none provided
|
420
|
+
if num_tapers is None:
|
421
|
+
num_tapers = math.floor(2 * time_bandwidth) - 1
|
422
|
+
|
423
|
+
# Warn if number of tapers is suboptimal
|
424
|
+
if num_tapers != math.floor(2 * time_bandwidth) - 1:
|
425
|
+
warnings.warn(
|
426
|
+
"Number of tapers is optimal at floor(2*TW) - 1. consider using "
|
427
|
+
+ str(math.floor(2 * time_bandwidth) - 1)
|
428
|
+
)
|
429
|
+
|
430
|
+
# If no window params provided, set to defaults
|
431
|
+
if window_params is None:
|
432
|
+
window_params = [5, 1]
|
433
|
+
|
434
|
+
# Check if window size is valid, fix if not
|
435
|
+
if window_params[0] * fs % 1 != 0:
|
436
|
+
winsize_samples = round(window_params[0] * fs)
|
437
|
+
warnings.warn(
|
438
|
+
"Window size is not divisible by sampling frequency. Adjusting window size to "
|
439
|
+
+ str(winsize_samples / fs)
|
440
|
+
+ " seconds"
|
441
|
+
)
|
442
|
+
else:
|
443
|
+
winsize_samples = window_params[0] * fs
|
444
|
+
|
445
|
+
# Check if window step is valid, fix if not
|
446
|
+
if window_params[1] * fs % 1 != 0:
|
447
|
+
winstep_samples = round(window_params[1] * fs)
|
448
|
+
warnings.warn(
|
449
|
+
"Window step size is not divisible by sampling frequency. Adjusting window step size to "
|
450
|
+
+ str(winstep_samples / fs)
|
451
|
+
+ " seconds"
|
452
|
+
)
|
453
|
+
else:
|
454
|
+
winstep_samples = window_params[1] * fs
|
455
|
+
|
456
|
+
# Get total data length
|
457
|
+
len_data = len(data)
|
458
|
+
|
459
|
+
# Check if length of data is smaller than window (bad)
|
460
|
+
if len_data < winsize_samples:
|
461
|
+
raise ValueError(
|
462
|
+
"\nData length ("
|
463
|
+
+ str(len_data)
|
464
|
+
+ ") is shorter than window size ("
|
465
|
+
+ str(winsize_samples)
|
466
|
+
+ "). Either increase data length or decrease window size."
|
467
|
+
)
|
468
|
+
|
469
|
+
# Find window start indices and num of windows
|
470
|
+
window_start = np.arange(0, len_data - winsize_samples + 1, winstep_samples)
|
471
|
+
num_windows = len(window_start)
|
472
|
+
|
473
|
+
# Get num points in FFT
|
474
|
+
if min_nfft == 0: # avoid divide by zero error in np.log2(0)
|
475
|
+
nfft = max(2 ** math.ceil(np.log2(abs(winsize_samples))), winsize_samples)
|
476
|
+
else:
|
477
|
+
nfft = max(
|
478
|
+
max(2 ** math.ceil(np.log2(abs(winsize_samples))), winsize_samples),
|
479
|
+
2 ** math.ceil(np.log2(abs(min_nfft))),
|
480
|
+
)
|
481
|
+
|
482
|
+
return [
|
483
|
+
data,
|
484
|
+
fs,
|
485
|
+
frequency_range,
|
486
|
+
time_bandwidth,
|
487
|
+
num_tapers,
|
488
|
+
int(winsize_samples),
|
489
|
+
int(winstep_samples),
|
490
|
+
window_start,
|
491
|
+
num_windows,
|
492
|
+
nfft,
|
493
|
+
detrend_opt,
|
494
|
+
plot_on,
|
495
|
+
verbose,
|
496
|
+
]
|
497
|
+
|
498
|
+
|
499
|
+
# PROCESS THE SPECTROGRAM PARAMETERS #
|
500
|
+
def process_spectrogram_params(fs, nfft, frequency_range, window_start, datawin_size):
|
501
|
+
"""Helper function to create frequency vector and window indices
|
502
|
+
Arguments:
|
503
|
+
fs (float): sampling frequency in Hz -- required
|
504
|
+
nfft (int): length of signal to calculate fft on -- required
|
505
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] -- required
|
506
|
+
window_start (1xm np array): array of timestamps representing the beginning time for each
|
507
|
+
window -- required
|
508
|
+
datawin_size (float): seconds in one window -- required
|
509
|
+
Returns:
|
510
|
+
window_idxs (nxm np array): indices of timestamps for each window
|
511
|
+
(nxm where n=number of windows and m=datawin_size)
|
512
|
+
stimes (1xt np array): array of times for the center of the spectral bins
|
513
|
+
sfreqs (1xf np array): array of frequency bins for the spectrogram
|
514
|
+
freq_inds (1d np array): boolean array of which frequencies are being analyzed in
|
515
|
+
an array of frequencies from 0 to fs with steps of fs/nfft
|
516
|
+
"""
|
517
|
+
|
518
|
+
# create frequency vector
|
519
|
+
df = fs / nfft
|
520
|
+
sfreqs = np.arange(0, fs, df)
|
521
|
+
|
522
|
+
# Get frequencies for given frequency range
|
523
|
+
freq_inds = (sfreqs >= frequency_range[0]) & (sfreqs <= frequency_range[1])
|
524
|
+
sfreqs = sfreqs[freq_inds]
|
525
|
+
|
526
|
+
# Compute times in the middle of each spectrum
|
527
|
+
window_middle_samples = window_start + round(datawin_size / 2)
|
528
|
+
stimes = window_middle_samples / fs
|
529
|
+
|
530
|
+
# Get indexes for each window
|
531
|
+
window_idxs = np.atleast_2d(window_start).T + np.arange(0, datawin_size, 1)
|
532
|
+
window_idxs = window_idxs.astype(int)
|
533
|
+
|
534
|
+
return [window_idxs, stimes, sfreqs, freq_inds]
|
535
|
+
|
536
|
+
|
537
|
+
# DISPLAY SPECTROGRAM PROPERTIES
|
538
|
+
def display_spectrogram_props(
|
539
|
+
fs,
|
540
|
+
time_bandwidth,
|
541
|
+
num_tapers,
|
542
|
+
data_window_params,
|
543
|
+
frequency_range,
|
544
|
+
nfft,
|
545
|
+
detrend_opt,
|
546
|
+
):
|
547
|
+
"""Prints spectrogram properties
|
548
|
+
Arguments:
|
549
|
+
fs (float): sampling frequency in Hz -- required
|
550
|
+
time_bandwidth (float): time-half bandwidth product (window duration*1/2*frequency_resolution) -- required
|
551
|
+
num_tapers (int): number of DPSS tapers to use -- required
|
552
|
+
data_window_params (list): 1x2 list - [window length(s), window step size(s)] -- required
|
553
|
+
frequency_range (list): 1x2 list - [<min frequency>, <max frequency>] -- required
|
554
|
+
nfft(float): number of fast fourier transform samples -- required
|
555
|
+
detrend_opt (str): detrend data window ('linear' (default), 'constant', 'off') -- required
|
556
|
+
Returns:
|
557
|
+
This function does not return anything
|
558
|
+
"""
|
559
|
+
|
560
|
+
data_window_params = np.asarray(data_window_params) / fs
|
561
|
+
|
562
|
+
# Print spectrogram properties
|
563
|
+
print("Multitaper Spectrogram Properties: ")
|
564
|
+
print(
|
565
|
+
" Spectral Resolution: "
|
566
|
+
+ str(2 * time_bandwidth / data_window_params[0])
|
567
|
+
+ "Hz"
|
568
|
+
)
|
569
|
+
print(" Window Length: " + str(data_window_params[0]) + "s")
|
570
|
+
print(" Window Step: " + str(data_window_params[1]) + "s")
|
571
|
+
print(" Time Half-Bandwidth Product: " + str(time_bandwidth))
|
572
|
+
print(" Number of Tapers: " + str(num_tapers))
|
573
|
+
print(
|
574
|
+
" Frequency Range: "
|
575
|
+
+ str(frequency_range[0])
|
576
|
+
+ "-"
|
577
|
+
+ str(frequency_range[1])
|
578
|
+
+ "Hz"
|
579
|
+
)
|
580
|
+
print(" NFFT: " + str(nfft))
|
581
|
+
print(" Detrend: " + detrend_opt + "\n")
|
582
|
+
|
583
|
+
|
584
|
+
# NANPOW2DB
|
585
|
+
def nanpow2db(y):
|
586
|
+
"""Power to dB conversion, setting bad values to nans
|
587
|
+
Arguments:
|
588
|
+
y (float or array-like): power
|
589
|
+
Returns:
|
590
|
+
ydB (float or np array): inputs converted to dB with 0s and negatives resulting in nans
|
591
|
+
"""
|
592
|
+
|
593
|
+
if isinstance(y, int) or isinstance(y, float):
|
594
|
+
if y == 0:
|
595
|
+
return np.nan
|
596
|
+
else:
|
597
|
+
ydB = 10 * np.log10(y)
|
598
|
+
else:
|
599
|
+
if isinstance(y, list): # if list, turn into array
|
600
|
+
y = np.asarray(y)
|
601
|
+
y = y.astype(float) # make sure it's a float array so we can put nans in it
|
602
|
+
y[y == 0] = np.nan
|
603
|
+
ydB = 10 * np.log10(y)
|
604
|
+
|
605
|
+
return ydB
|
606
|
+
|
607
|
+
|
608
|
+
# Helper #
|
609
|
+
def is_outlier(data):
|
610
|
+
smad = 1.4826 * np.median(
|
611
|
+
abs(data - np.median(data))
|
612
|
+
) # scaled median absolute deviation
|
613
|
+
outlier_mask = (
|
614
|
+
abs(data - np.median(data)) > 3 * smad
|
615
|
+
) # outliers are more than 3 smads away from median
|
616
|
+
outlier_mask = outlier_mask | np.isnan(data) | np.isinf(data)
|
617
|
+
return outlier_mask
|
618
|
+
|
619
|
+
|
620
|
+
# CALCULATE MULTITAPER SPECTRUM ON SINGLE SEGMENT
|
621
|
+
def calc_mts_segment(
|
622
|
+
data_segment,
|
623
|
+
dpss_tapers,
|
624
|
+
nfft,
|
625
|
+
freq_inds,
|
626
|
+
detrend_opt,
|
627
|
+
num_tapers,
|
628
|
+
dpss_eigen,
|
629
|
+
weighting,
|
630
|
+
wt,
|
631
|
+
):
|
632
|
+
"""Helper function to calculate the multitaper spectrum of a single segment of data
|
633
|
+
Arguments:
|
634
|
+
data_segment (1d np.array): One window worth of time-series data -- required
|
635
|
+
dpss_tapers (2d np.array): Parameters for the DPSS tapers to be used.
|
636
|
+
Dimensions are (num_tapers, winsize_samples) -- required
|
637
|
+
nfft (int): length of signal to calculate fft on -- required
|
638
|
+
freq_inds (1d np array): boolean array of which frequencies are being analyzed in
|
639
|
+
an array of frequencies from 0 to fs with steps of fs/nfft
|
640
|
+
detrend_opt (str): detrend data window ('linear' (default), 'constant', 'off')
|
641
|
+
num_tapers (int): number of tapers being used
|
642
|
+
dpss_eigen (np array):
|
643
|
+
weighting (str):
|
644
|
+
wt (int or np array):
|
645
|
+
Returns:
|
646
|
+
mt_spectrum (1d np.array): spectral power for single window
|
647
|
+
"""
|
648
|
+
|
649
|
+
# If segment has all zeros, return vector of zeros
|
650
|
+
if all(data_segment == 0):
|
651
|
+
ret = np.empty(sum(freq_inds))
|
652
|
+
ret.fill(0)
|
653
|
+
return ret
|
654
|
+
|
655
|
+
if any(np.isnan(data_segment)):
|
656
|
+
ret = np.empty(sum(freq_inds))
|
657
|
+
ret.fill(np.nan)
|
658
|
+
return ret
|
659
|
+
|
660
|
+
# Option to detrend data to remove low frequency DC component
|
661
|
+
if detrend_opt != "off":
|
662
|
+
data_segment = detrend(data_segment, type=detrend_opt)
|
663
|
+
|
664
|
+
# Multiply data by dpss tapers (STEP 2)
|
665
|
+
tapered_data = np.multiply(np.mat(data_segment).T, np.mat(dpss_tapers.T))
|
666
|
+
|
667
|
+
# Compute the FFT (STEP 3)
|
668
|
+
fft_data = np.fft.fft(tapered_data, nfft, axis=0)
|
669
|
+
|
670
|
+
# Compute the weighted mean spectral power across tapers (STEP 4)
|
671
|
+
spower = np.power(np.imag(fft_data), 2) + np.power(np.real(fft_data), 2)
|
672
|
+
if weighting == "adapt":
|
673
|
+
# adaptive weights - for colored noise spectrum (Percival & Walden p368-370)
|
674
|
+
tpower = np.dot(np.transpose(data_segment), (data_segment / len(data_segment)))
|
675
|
+
spower_iter = np.mean(spower[:, 0:2], 1)
|
676
|
+
spower_iter = spower_iter[:, np.newaxis]
|
677
|
+
a = (1 - dpss_eigen) * tpower
|
678
|
+
for i in range(3): # 3 iterations only
|
679
|
+
# Calc the MSE weights
|
680
|
+
b = np.dot(spower_iter, np.ones((1, num_tapers))) / (
|
681
|
+
(np.dot(spower_iter, np.transpose(dpss_eigen)))
|
682
|
+
+ (np.ones((nfft, 1)) * np.transpose(a))
|
683
|
+
)
|
684
|
+
# Calc new spectral estimate
|
685
|
+
wk = (b**2) * np.dot(np.ones((nfft, 1)), np.transpose(dpss_eigen))
|
686
|
+
spower_iter = np.sum((np.transpose(wk) * np.transpose(spower)), 0) / np.sum(
|
687
|
+
wk, 1
|
688
|
+
)
|
689
|
+
spower_iter = spower_iter[:, np.newaxis]
|
690
|
+
|
691
|
+
mt_spectrum = np.squeeze(spower_iter)
|
692
|
+
|
693
|
+
else:
|
694
|
+
# eigenvalue or uniform weights
|
695
|
+
mt_spectrum = np.dot(spower, wt)
|
696
|
+
mt_spectrum = np.reshape(mt_spectrum, nfft) # reshape to 1D
|
697
|
+
|
698
|
+
return mt_spectrum[freq_inds]
|
699
|
+
|
700
|
+
|
701
|
+
def plot_TFR(
|
702
|
+
ax,
|
703
|
+
stimes,
|
704
|
+
sfreqs,
|
705
|
+
spect,
|
706
|
+
psd_go=False,
|
707
|
+
cmap="cet_rainbow4",
|
708
|
+
clim_scale=[0, 100],
|
709
|
+
cbar_width=0.05,
|
710
|
+
cbar_pad=0.1,
|
711
|
+
cbar_loc="right",
|
712
|
+
cbar_fontsize=11,
|
713
|
+
cbar_shrink= None,
|
714
|
+
cbar_label=None,
|
715
|
+
**kwargs
|
716
|
+
):
|
717
|
+
"""
|
718
|
+
**kwargs: cbar_kw=dict(ticks=,....)
|
719
|
+
The Colorbar class in Matplotlib: These are just a few examples, and there may be additional keyword arguments
|
720
|
+
cax:
|
721
|
+
Specifies the Axes where the colorbar will be drawn.
|
722
|
+
mappable:
|
723
|
+
Specifies the mappable object (e.g., ScalarMappable) that the colorbar represents.
|
724
|
+
orientation:
|
725
|
+
Specifies the orientation of the colorbar (e.g., 'vertical' or 'horizontal').
|
726
|
+
ticks:
|
727
|
+
Specifies the tick locations on the colorbar.
|
728
|
+
format:
|
729
|
+
Specifies the format string for the tick labels.
|
730
|
+
extend:
|
731
|
+
Specifies whether to add extensions to the colorbar indicating out-of-range values.
|
732
|
+
extendfrac:
|
733
|
+
Specifies the length of the extensions as a fraction of the colorbar.
|
734
|
+
extendrect:
|
735
|
+
Specifies whether to use rectangular extensions.
|
736
|
+
spacing:
|
737
|
+
Specifies the spacing between the colorbar and the plot.
|
738
|
+
drawedges:
|
739
|
+
Specifies whether to draw lines around the colorbar.
|
740
|
+
filled:
|
741
|
+
Specifies whether the colorbar is filled with color.
|
742
|
+
shrink:
|
743
|
+
Specifies the shrinkage factor of the colorbar relative to the Axes.
|
744
|
+
aspect:
|
745
|
+
Specifies the aspect ratio of the colorbar.
|
746
|
+
pad:
|
747
|
+
Specifies the padding between the colorbar and the Axes.
|
748
|
+
anchor:
|
749
|
+
Specifies the anchor point of the colorbar.
|
750
|
+
panchor:
|
751
|
+
Specifies the anchor point of the parent Axes.
|
752
|
+
fraction:
|
753
|
+
Specifies the fraction of the Axes to use for the colorbar.
|
754
|
+
boundaries:
|
755
|
+
Specifies the boundaries for the color levels.
|
756
|
+
norm:
|
757
|
+
Specifies the normalization instance used to scale data values to the colormap.
|
758
|
+
alpha:
|
759
|
+
Specifies the transparency of the colorbar.
|
760
|
+
"""
|
761
|
+
print(
|
762
|
+
f"\ntimes.shape:{stimes.shape}, \nfreqs.shape:{sfreqs.shape},\npows.shape:{spect.shape}"
|
763
|
+
)
|
764
|
+
if cbar_label is None:
|
765
|
+
cbar_label = "PSD (dB)" if psd_go else "Power"
|
766
|
+
spect_data = nanpow2db(spect) if psd_go else spect # convert from power to dB
|
767
|
+
|
768
|
+
# Set x and y axes
|
769
|
+
dx = stimes[1] - stimes[0]
|
770
|
+
dy = sfreqs[1] - sfreqs[0]
|
771
|
+
extent = [stimes[0] - dx, stimes[-1] + dx, sfreqs[-1] + dy, sfreqs[0] - dy]
|
772
|
+
im = ax.imshow(
|
773
|
+
spect_data, extent=extent, aspect="auto", cmap=cmap
|
774
|
+
) # "cet_rainbow4"
|
775
|
+
|
776
|
+
ax.invert_yaxis()
|
777
|
+
# Convert float value of cbar_width to a string with a percentage sign appended
|
778
|
+
if isinstance(cbar_width, float):
|
779
|
+
if cbar_width > 1:
|
780
|
+
cbar_width = f"{cbar_width}%"
|
781
|
+
else:
|
782
|
+
cbar_width = f"{cbar_width * 100}%"
|
783
|
+
else:
|
784
|
+
cbar_width = cbar_width
|
785
|
+
# Create divider for existing axes instance
|
786
|
+
divider = make_axes_locatable(ax)
|
787
|
+
# Append axes to the right of ax, with some width
|
788
|
+
cax = divider.append_axes(
|
789
|
+
cbar_loc, size=cbar_width, pad=cbar_pad
|
790
|
+
) # cbar_width="2%"
|
791
|
+
# colorbar
|
792
|
+
cbar_kw=kwargs.get('cbar_kw',{})
|
793
|
+
cbar = plt.colorbar(im, cax=cax,**cbar_kw)
|
794
|
+
# cbar.ax.set_aspect('auto') # Setting colorbar shrink
|
795
|
+
cbar.set_label(cbar_label,fontsize=cbar_fontsize) # Set colorbar label
|
796
|
+
# Adjust colorbar width
|
797
|
+
cbar.ax.tick_params(labelsize=cbar_fontsize) # Adjust font size of colorbar ticks
|
798
|
+
|
799
|
+
im.set_clim(np.percentile(spect_data, clim_scale)) # from 5th percentile to 98th
|
800
|
+
return ax
|