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.
Files changed (64) hide show
  1. py2ls/.git/COMMIT_EDITMSG +1 -0
  2. py2ls/.git/FETCH_HEAD +1 -0
  3. py2ls/.git/HEAD +1 -0
  4. py2ls/.git/config +15 -0
  5. py2ls/.git/description +1 -0
  6. py2ls/.git/hooks/applypatch-msg.sample +15 -0
  7. py2ls/.git/hooks/commit-msg.sample +24 -0
  8. py2ls/.git/hooks/fsmonitor-watchman.sample +174 -0
  9. py2ls/.git/hooks/post-update.sample +8 -0
  10. py2ls/.git/hooks/pre-applypatch.sample +14 -0
  11. py2ls/.git/hooks/pre-commit.sample +49 -0
  12. py2ls/.git/hooks/pre-merge-commit.sample +13 -0
  13. py2ls/.git/hooks/pre-push.sample +53 -0
  14. py2ls/.git/hooks/pre-rebase.sample +169 -0
  15. py2ls/.git/hooks/pre-receive.sample +24 -0
  16. py2ls/.git/hooks/prepare-commit-msg.sample +42 -0
  17. py2ls/.git/hooks/push-to-checkout.sample +78 -0
  18. py2ls/.git/hooks/update.sample +128 -0
  19. py2ls/.git/index +0 -0
  20. py2ls/.git/info/exclude +6 -0
  21. py2ls/.git/logs/HEAD +1 -0
  22. py2ls/.git/logs/refs/heads/main +1 -0
  23. py2ls/.git/logs/refs/remotes/origin/HEAD +1 -0
  24. py2ls/.git/logs/refs/remotes/origin/main +1 -0
  25. py2ls/.git/objects/25/b796accd261b9135fd32a2c00785f68edf6c46 +0 -0
  26. py2ls/.git/objects/36/b4a1b7403abc6c360f8fe2cb656ab945254971 +0 -0
  27. py2ls/.git/objects/3f/d6561300938afbb3d11976cf9c8f29549280d9 +0 -0
  28. py2ls/.git/objects/58/20a729045d4dc7e37ccaf8aa8eec126850afe2 +0 -0
  29. py2ls/.git/objects/60/f273eb1c412d916fa3f11318a7da7a9911b52a +0 -0
  30. py2ls/.git/objects/61/570cec8c061abe74121f27f5face6c69b98f99 +0 -0
  31. py2ls/.git/objects/69/13c452ca319f7cbf6a0836dc10a5bb033c84e4 +0 -0
  32. py2ls/.git/objects/78/3d4167bc95c9d2175e0df03ef1c1c880ba75ab +0 -0
  33. py2ls/.git/objects/79/7ae089b2212a937840e215276005ce76881307 +0 -0
  34. py2ls/.git/objects/7e/5956c806b5edc344d46dab599dec337891ba1f +1 -0
  35. py2ls/.git/objects/8e/55a7d2b96184030211f20c9b9af201eefcac82 +0 -0
  36. py2ls/.git/objects/91/c69ad88fe0ba94aa7859fb5f7edac5e6f1a3f7 +0 -0
  37. py2ls/.git/objects/b0/56be4be89ba6b76949dd641df45bb7036050c8 +0 -0
  38. py2ls/.git/objects/b0/9cd7856d58590578ee1a4f3ad45d1310a97f87 +0 -0
  39. py2ls/.git/objects/d9/005f2cc7fc4e65f14ed5518276007c08cf2fd0 +0 -0
  40. py2ls/.git/objects/df/e0770424b2a19faf507a501ebfc23be8f54e7b +0 -0
  41. py2ls/.git/objects/e9/391ffe371f1cc43b42ef09b705d9c767c2e14f +0 -0
  42. py2ls/.git/objects/fc/292e793ecfd42240ac43be407023bd731fa9e7 +0 -0
  43. py2ls/.git/refs/heads/main +1 -0
  44. py2ls/.git/refs/remotes/origin/HEAD +1 -0
  45. py2ls/.git/refs/remotes/origin/main +1 -0
  46. py2ls/.gitattributes +2 -0
  47. py2ls/.gitignore +152 -0
  48. py2ls/LICENSE +201 -0
  49. py2ls/README.md +409 -0
  50. py2ls/__init__.py +17 -0
  51. py2ls/brain_atlas.py +145 -0
  52. py2ls/correlators.py +475 -0
  53. py2ls/dbhandler.py +97 -0
  54. py2ls/freqanalysis.py +800 -0
  55. py2ls/internet_finder.py +405 -0
  56. py2ls/ips.py +2844 -0
  57. py2ls/netfinder.py +780 -0
  58. py2ls/sleep_events_detectors.py +1350 -0
  59. py2ls/translator.py +686 -0
  60. py2ls/version.py +1 -0
  61. py2ls/wb_detector.py +169 -0
  62. py2ls-0.1.0.dist-info/METADATA +12 -0
  63. py2ls-0.1.0.dist-info/RECORD +64 -0
  64. 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