accusleepy 0.1.0__py3-none-any.whl

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