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