├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── am_analysis ├── __init__.py ├── am_analysis.py ├── explore_stfft_ama_gui.py ├── explore_wavelet_ama_gui.py └── msqi_ama.py ├── example_01.py ├── example_02.py ├── example_03.py ├── example_04.py ├── example_05.py ├── example_data ├── ecg_data.pkl ├── eeg_data.pkl ├── info.txt ├── msqi_ecg_data.pkl └── p234_004.wav ├── example_msqi.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for contributing to this repository. 4 | 5 | When contributing, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 6 | 7 | ## How to contribute. 8 | The general process to contribute to an open source project can be found in detail in this great post: [https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/](https://akrabat.com/the-beginners-guide-to-contributing-to-a-github-project/). 9 | 10 | In summary, the process consists on 7 steps: 11 | 12 | 1. Fork the repository to your GitHub account and clone it to your computer. 13 | 14 | ``` 15 | $ git clone https://github.com/USERNAME/FORK.git 16 | ``` 17 | 18 | 2. Create a upstream remote (to this repository) and sync your local copy. 19 | 20 | ``` 21 | $ git remote add upstream https://github.com/MuSAELab/amplitude-modulation-analysis-module.git 22 | ``` 23 | 24 | At this point `origin` refers to your forked repository (in your account) and `upstream` to the repository in [https://github.com/MuSAELab](https://github.com/MuSAELab) 25 | 26 | ``` 27 | $ git checkout master 28 | $ git pull upstream master && git push origin master 29 | ``` 30 | 3. Create a branch in your local copy. 31 | 32 | ``` 33 | git checkout -b fixing-something 34 | ``` 35 | 36 | 4. Perform the change, write good commit messages. 37 | 38 | 5. Push your branch to your fork in GitHub. 39 | 40 | ``` 41 | git push -u origin fixing-something 42 | ``` 43 | 44 | 6. Create a new Pull Request. 45 | 46 | 7. Feedback and to merge your work to the `upstream` repository 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Raymundo Cassani, Isabela Albuquerque and João Monteio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amplitude Modulation Analysis Module 2 | 3 | The amplitude modulation analysis module for **Python 3**, provides functions to compute and visualize the frequency-frequency-domain representation of real-valued signals. The **MATLAB-Octave** version of this module can be found here: [https://github.com/MuSAELab/amplitude-modulation-analysis-toolbox](https://github.com/MuSAELab/amplitude-modulation-analysis-toolbox) 4 | 5 | The module includes a GUI implementation, which facilitates the amplitude modulation analysis by allowing changing parameters on line. 6 | 7 | In summary, the frequency-frequency representation of a signal is computed by performing two transformations. 8 | 9 | ![diagram](https://user-images.githubusercontent.com/8238803/35760392-c74639f6-084d-11e8-8d34-396324e9b045.png) 10 | Signal processing steps involved in the calculation of the modulation spectrogram from the amplitude spectrogram of signal. The block |abs| indicates the absolute value, and the FT indicates the use of the Fourier transform. 11 | 12 | This module provides two implementations for the time to time-frequency transformation (*First Transformation*), one based on the STFFT, and the other on the continuous wavelet transform (CWT) using the Complex Morlet wavelet. The time-frequency to frequency-frequency transformation (*Second Transformation*) is carried out with the FFT. 13 | 14 | ## Installation 15 | Dowload or clone the respository, then: 16 | `$ pip install .` 17 | 18 | ## Examples 19 | Besides the functions to compute and visualize the frequency-frequency representation of real signals, example data and scripts are provided. 20 | 21 | ### Example 1: `example_01.py` 22 | This example shows the usage and the commands accepted by GUI to explore amplitude modulation for a example ECG and EEG data. The GUI can be called with the functions: 23 | `explore_strfft_am_gui()` which uses STFFT, and `explore_wavelet_am_gui()` based on wavelet transformation. Further details in their use refer to the comments in `example_01.py`. 24 | 25 | ![stfft](https://user-images.githubusercontent.com/8238803/35760391-c4cee66e-084d-11e8-977d-48f757f72495.png) 26 | STFFT-based Amplitude Modulation analysis GUI 27 |
28 | 29 | ![wavelet](https://user-images.githubusercontent.com/8238803/35760382-b1116886-084d-11e8-864e-155ba5359c65.png) 30 | CWT-based Amplitude Modulation analysis GUI 31 | 32 | ### Example 2: `example_02.py` 33 | This script shows presents the details in the usage of the functions in the module to carry on the signal transformations, as well as plotting functions. Refer to the comments in `example_02.py` 34 | 35 | ### Acknowledgement 36 | The research is based upon work supported by the Office of the Director of National Intelligence (ODNI), Intelligence Advanced Research Projects Activity (IARPA), via IARPA Contract N°2017 - 17042800005 . The views and conclusions con - tained herein are thos e of the authors and should not be interpreted as necessarily representing the official policies or endorsements, either expressed or implied, of the ODNI, IARPA, or the U.S. Government. The U.S. Government is authorized to reproduce and distribute reprint s for Governmental purposes notwithstanding any copyright annotation thereon. 37 | -------------------------------------------------------------------------------- /am_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /am_analysis/am_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Amplitude Modulation Analysis Toolbox 3 | """ 4 | 5 | import numpy as np 6 | import scipy.signal 7 | import matplotlib.pyplot as plt 8 | import matplotlib.ticker as ticker 9 | 10 | def conv_m(a, b, mode='full'): 11 | """Convolve a vector with collection of vectors. 12 | 13 | Convolve a 1D array `a` with each column of the 2D array `b`. 14 | 15 | Convolution is carried out with `scipy.signal.fftconvolve` 16 | 17 | Parameters 18 | ---------- 19 | a : 1D array 20 | 1D array input 21 | 22 | b : 1D or 2D array_like 23 | 1D or 2D array input 24 | 25 | mode : str {'full', 'same'}, optional 26 | A string indicating the size of the output: 27 | 28 | ``full`` 29 | The output is the full discrete linear convolution 30 | of the inputs. (Default) 31 | ``same`` 32 | The output is the same size as `a`, centered 33 | with respect to the 'full' output. 34 | 35 | Returns 36 | ------- 37 | c : 2D array 38 | A 2D array where each columns corresponds to the 39 | convolution of `a` and a column of `b` 40 | 41 | See Also 42 | -------- 43 | `scipy.signal.fftconvolve()` 44 | 45 | """ 46 | # input vector 'a' to 1 dimension 47 | a = a.ravel() 48 | # number of samples vector 'a' 49 | siz_a = len(a) 50 | 51 | # input 'b' as 2D matrix [samples, columns] 52 | try: 53 | b.shape[1] 54 | except IndexError: 55 | b = b[:, np.newaxis] 56 | 57 | # number of samples and number of channels in input 'b' 58 | siz_b, col_b = b.shape 59 | 60 | # allocate space for result 61 | if mode == 'same': 62 | c = np.zeros((siz_a, col_b) , dtype = complex) 63 | elif mode == 'full': 64 | N = siz_a + siz_b - 1 65 | c = np.zeros((N , col_b), dtype = complex) 66 | 67 | # 1D convolutions per columns in 'b' 68 | for ix in range(0 , col_b): 69 | c[:,ix] = scipy.signal.fftconvolve(a, b[:,ix] , mode) 70 | 71 | return c 72 | 73 | 74 | def epoching(data, samples_epoch, samples_overlap = 0): 75 | """Divide an array in a colletion of smaller arrays 76 | 77 | Divides the `data` provided as [n_samples, n_channels] using the 78 | `size_epoch` indicated (in samples) and the `overlap_epoch` between 79 | consecutive epochs. 80 | 81 | Parameters 82 | ---------- 83 | data : 2D array 84 | with shape (n_samples, n_channels) 85 | 86 | samples_epochs : 87 | number of samples in smaller epochs 88 | 89 | samples_overlap : 90 | number of samples for ovelap between epochs (Default 0) 91 | 92 | 93 | Returns 94 | ------- 95 | epochs : 3D array 96 | with shape (samples_epoch, n_channels, n_epochs) 97 | 98 | remainder : 2D array 99 | with the remaining data after last complete epoch 100 | 101 | ix_center : 1D array 102 | indicates the index tha corresponds to the center of the nth epoch. 103 | 104 | """ 105 | # input 'data' as 2D matrix [samples, columns] 106 | try: 107 | data.shape[1] 108 | except IndexError: 109 | data = data[:, np.newaxis] 110 | 111 | # number of samples and number of channels 112 | n_samples, n_channels = data.shape 113 | 114 | # Size of half epoch 115 | half_epoch = np.ceil(samples_epoch / 2 ) 116 | 117 | # Epoch shift 118 | samples_shift = samples_epoch - samples_overlap 119 | 120 | # Number of epochs 121 | n_epochs = int(np.floor( (n_samples - samples_epoch) / float(samples_shift) ) + 1 ) 122 | if n_epochs == 0: 123 | return np.array([]), data, np.array([]) 124 | 125 | #markers indicates where the epoch starts, and the epoch contains samples_epoch rows 126 | markers = np.asarray(range(0,n_epochs)) * samples_shift 127 | markers = markers.astype(int) 128 | 129 | #Divide data in epochs 130 | epochs = np.zeros((samples_epoch, n_channels, n_epochs)) 131 | ix_center = np.zeros((n_epochs,1)) 132 | 133 | for i_epoch in range(0,n_epochs): 134 | epochs[:,:,i_epoch] = data[ markers[i_epoch] : markers[i_epoch] + samples_epoch ,:] 135 | ix_center[i_epoch] = markers[i_epoch] -1 + half_epoch 136 | 137 | if ( (markers[-1] + samples_epoch) < n_samples): 138 | remainder = data[markers[-1] + samples_epoch : n_samples, :] 139 | else: 140 | remainder = np.asarray([]) 141 | 142 | return epochs, remainder, ix_center.astype(int) 143 | 144 | def iepoching(epochs, shift_epoch): 145 | """ 146 | Merges a set of epochs [n_samples_epoch, n_channels] into 147 | the complete signal(s) x [n_samples, n_channels] taking into account 148 | the shift between consecutive epochs 149 | 150 | Parameters 151 | ---------- 152 | epochs : 2D array_like with shape (n_samples, n_channels) 153 | shift_epoch : number of samples in smaller epochs 154 | 155 | Returns 156 | ------- 157 | x : 2D array with shape (samples_epoch, n_channels, n_epochs) 158 | 159 | """ 160 | 161 | # obtain parameters 162 | (size_epoch, n_channels, n_epochs) = epochs.shape 163 | n_samples = (shift_epoch * (n_epochs - 1)) + size_epoch 164 | ix = np.arange(n_epochs) * shift_epoch 165 | 166 | # merging matrix 167 | merging = np.zeros((n_samples, n_channels, 2)) 168 | # Number of epochs that contribute for a specific point 169 | n_merging = np.zeros((n_samples, n_channels, 2)) 170 | 171 | for i_epoch in range(n_epochs): 172 | merging[ix[i_epoch] : ix[i_epoch] + size_epoch, :, 1 ] = epochs[:, :, i_epoch] 173 | n_merging[ix[i_epoch] : ix[i_epoch] + size_epoch, :, 1] = 1 174 | merging[:,:,0] = np.sum(merging, axis=2) 175 | n_merging[:,:,0] = np.sum(n_merging, axis=2) 176 | merging[ix[i_epoch] : ix[i_epoch] + size_epoch, :, 1 ] = 0 177 | n_merging[ix[i_epoch] : ix[i_epoch] + size_epoch, :, 1 ] = 0 178 | 179 | x = np.divide(merging[:,:,0], n_merging[:,:,0]) 180 | return x 181 | 182 | 183 | def cmorlet_wavelet(x, fs, freq_vct, n=6, normalization=True): 184 | """Perform the continuous wavelet (CWT) tranform using the complex Morlet wavelet. 185 | 186 | Parameters 187 | ---------- 188 | x : 1D array with shape (n_samples) or 189 | 2D array with shape (n_samples, n_channels) 190 | fs : Sampling frequency 191 | in Hz 192 | freq_vct : 1D array 193 | with frequencies to compute the CWT (Default = [1 : 1 : fs/2] ) 194 | n : Number of cicles inside the Gaussian curve 195 | (Default 6) 196 | normalization : Scale each wavelet to have energy equal to 1 197 | (Default True) 198 | 199 | 200 | Returns 201 | ------- 202 | wcoef : Complex wavelet coefficients 203 | 2D array with shape [n_samples, n_freqs] if `x` is 1D array 204 | 3D array with shape [n_samples, n_freqs, n_channels] if `x` is 2D array 205 | 206 | wfam : 2D array with shape [n_wavelet_samples, n_freqs] where each column 207 | corresponds to the a member of the wavelet family 208 | 209 | """ 210 | # input 'x' as 2D matrix [samples, columns] 211 | try: 212 | x.shape[1] 213 | except IndexError: 214 | x = x[:, np.newaxis] 215 | 216 | # number of samples and number of channels 217 | n_samples, n_channels = x.shape 218 | 219 | # number of wavelets 220 | n_freqs = len(freq_vct) 221 | 222 | # number of samples for Wavetet family 223 | # This is equal to the number of samples needed to represent 2*n cycles 224 | # of a sine with frequency = fres(1)[Hz], sampled at fs [Hz]. 225 | # This is done to ensure that every wavelet in the wavalet family will be 226 | # close to 0 in the negative and positive edges 227 | n_samples_wav = np.round( (2*n/freq_vct[0])*fs ) 228 | 229 | # The wavelet will be symmetrical around 0 230 | if np.mod(n_samples_wav,2) == 0: # even samples 231 | n_samples_wav = n_samples_wav + 1 232 | 233 | # create time vector for Wavelet family 234 | half = np.floor(n_samples_wav/2) 235 | time = np.arange(-half, half+1)/fs 236 | 237 | # initialize Wavelet family matrix 238 | wfam = np.zeros([len(time), n_freqs], dtype=complex) 239 | 240 | # for each frequency defined in FREQ, create its respective Wavelet 241 | for iwav in range(n_freqs): 242 | s = n/(2*np.pi*freq_vct[iwav]) 243 | gaussian_win = np.exp((-time**2)/(2*s**2)) 244 | sinwave = np.exp(2*np.pi*1j*freq_vct[iwav]*time) 245 | if normalization: 246 | # each wavelet has unit energy sum(abs(wavelet).^2)) = 1 247 | A = 1. / ((s**2) * np.pi) ** (1./4) 248 | else: 249 | A = 1. 250 | # Complex Morlet wavelet 251 | wfam[:, iwav] = A * sinwave * gaussian_win 252 | 253 | wcoef = np.zeros((n_samples, n_freqs, n_channels), dtype=complex) 254 | 255 | if n_channels == 1: 256 | # one channel 257 | tmp = conv_m(x, wfam, 'same') 258 | wcoef[:, :, 0] = tmp 259 | else: 260 | # convolution between signal X and the each Wavelt in the Wavelet family 261 | for i_channel in range(n_channels): 262 | x_tmp = x[:, i_channel] 263 | tmp = conv_m(x_tmp, wfam, 'same') 264 | wcoef[:, :, i_channel] = tmp 265 | 266 | return wcoef, wfam 267 | 268 | 269 | def rfft(x, n=None, dim=None): 270 | """Real Fast Fourier Transform. 271 | 272 | Considering a real signal A with B = fft(A), B is Hermitian symmetric, 273 | i.e. B(-1) = conj(B(1)), therefore the complete spectrum B 274 | can be found by using with only the non-negative frequencies in B 275 | 276 | 277 | Parameters 278 | ---------- 279 | x : 1D array with shape (n_samples) or 280 | 2D array with shape (n_samples, n_channels) 281 | 282 | n : Number of samples to compute the FFT 283 | (Default = n_samples in array x) 284 | dim : Dimension to compute the RFFT 285 | (Default: first array dimension whose size does not equal 1) 286 | 287 | Returns 288 | ------- 289 | y : Non-negative complex spectrum of `x` with shape as `x` 290 | 291 | See Also 292 | -------- 293 | `np.fft.fft()` 294 | 295 | """ 296 | 297 | # shape of x 298 | shape_x = x.shape 299 | # number of dimentions 300 | dim_x = len(shape_x) 301 | 302 | # limits to 2-dimention data 303 | assert dim_x<=2 304 | 305 | # check shape of X, and set n and dim defaults 306 | if dim_x == 1: 307 | dim_def = 0 308 | else: 309 | if shape_x[0] == 1: 310 | # shape [1, n_samples] (row vector) 311 | dim_def = 1 312 | elif shape_x[1] == 1: 313 | # shape [n_samples, 1] (column vector) 314 | dim_def = 0 315 | else: 316 | # X is a 2D Matrix, a shape [n_samples, n_channels] is asummed 317 | dim_def = 0 318 | 319 | if dim is None: 320 | dim = dim_def 321 | 322 | if n is None: 323 | n = shape_x[dim] 324 | 325 | # FFT 326 | yc = np.fft.fft(x, n=n, axis=dim) 327 | 328 | # points to keep 329 | if n%2 == 0: 330 | # even case 331 | n_new = int((n / 2) + 1) 332 | else: 333 | # odd case 334 | n_new = int((n + 1) / 2) 335 | 336 | if dim_x == 1: 337 | y = yc[0:n_new] 338 | else: 339 | if dim == 0: 340 | y = yc[0:n_new,:] 341 | else: 342 | y = yc[:, 0:n_new] 343 | 344 | return y 345 | 346 | def irfft(y, n=None, dim=None): 347 | ''' 348 | The IRFFT function returns the Inverse DFT (using the RFFT algorithm)of 349 | a spectrum Y containing ONLY the positive frequencies, with the 350 | assumption than Y is the positive half of a Hermitian Symmetric spectrum 351 | from a real signal X. 352 | 353 | Parameters 354 | ---------- 355 | y : 1D or 2D array with the positive spectrum of 356 | real-valued signals with shape (n_samples, n_channels) 357 | n : Number of samples in the original x signals 358 | N not provided. Y is assumed be obtained from a signal X with even number fo samples 359 | dim : Dimension to compute the IRFFT (Default: first array dimension whose size does not equal 1) 360 | 361 | Returns 362 | ------- 363 | x : Real-valued signal(s) 364 | 365 | See Also 366 | -------- 367 | `np.fft.ifft()` 368 | ''' 369 | 370 | # verify y 371 | shape_y = y.shape 372 | # number of dimentions 373 | dim_y = len(shape_y) 374 | 375 | # limits to 2-dimention data 376 | assert dim_y<=2 377 | 378 | # check shape of y, and set n and dim defaults 379 | if dim_y == 1: 380 | dim_def = 0 381 | else: 382 | if shape_y[0] == 1: 383 | # shape [1, n_samples] (row vector) 384 | dim_def = 1 385 | elif shape_y[1] == 1: 386 | # shape [n_samples, 1] (column vector) 387 | dim_def = 0 388 | else: 389 | # X is a 2D Matrix, a shape [n_samples, n_channels] is asummed 390 | dim_def = 0 391 | 392 | if dim is None: 393 | dim = dim_def 394 | 395 | # verify 'n' number-of-samples parameter 396 | if n is None: 397 | print('N not provided. Y is assumed be obtained from a signal X with even number fo samples') 398 | n_half = shape_y[dim] 399 | n = (n_half - 1) * 2 400 | 401 | # reconstruct missing half of Spectrum 402 | if np.mod(n,2) == 0: 403 | # number of samples is even 404 | n_half = (n / 2) + 1 405 | ix_limit = slice(1, -1 ) 406 | else: 407 | # number of samples is odd 408 | n_half = (n + 1) / 2 409 | ix_limit = slice(1, None) 410 | 411 | if dim_y == 1: 412 | # spectrum in y is 1D 413 | y_neg = np.conj(np.flipud(y[ix_limit])) 414 | yc = np.concatenate((y, y_neg), axis=0) 415 | else: 416 | # check shape of y, and add negative frequencies 417 | if dim == 0: 418 | # spectra in y are column wise 419 | y_neg = np.conj(np.flipud(y[ix_limit, :])) 420 | yc = np.concatenate((y, y_neg), axis=0) 421 | else: 422 | # spectra in y are row-wise 423 | y_neg = np.conj(np.fliplr(y[:, ix_limit])) 424 | yc = np.concatenate((y, y_neg), axis=1) 425 | 426 | x = np.real(np.fft.ifft(yc, n, dim)) 427 | 428 | return x 429 | 430 | 431 | def rfft_psd(x, fs, n_fft=None, win_function = 'hamming', channel_names=None): 432 | """ This function computes the PSD for one or a set of REAL signals. 433 | 434 | Parameters 435 | ---------- 436 | x : 1D array with shape (n_samples) or 437 | 2D array with shape (n_samples, n_channels) 438 | fs : Sampling frequency 439 | in Hz 440 | n_fft : Number of samples to compute the FFT 441 | (Default = n_samples in array x) 442 | win_function : Window function applied to the signal 443 | (Default 'Hamming') 444 | channel_names : Names of the signals 445 | (Default Signal-XX with XX 1, 2, ... n_channels) 446 | 447 | 448 | Returns 449 | ------- 450 | psd_data : Dictionary with PSD data, with the elements: 451 | rFFT 452 | First half of the FFT(x) (u), scaled by the Window RMS 453 | PSD 454 | Power Spectrum Density (u^2 / Hz) 455 | fs 456 | Sampling frequency (Hz) 457 | freq_axis 458 | Frequency axis for rFFT and PSD (Hz) 459 | freq_delta 460 | Frequency axis step (Hz) 461 | n_samples 462 | Number of samples of the signal or signals 'x' 463 | n_fft 464 | Number of elements utilized to perform FFT 465 | win_function 466 | Window applied to the data in 'x' 467 | channel_names 468 | Names of channels 469 | 470 | """ 471 | 472 | # input 'x' as 2D matrix [samples, columns] 473 | try: 474 | x.shape[1] 475 | except IndexError: 476 | x = x[:, np.newaxis] 477 | 478 | # number of samples and number of channels 479 | n_samples, n_channels = x.shape 480 | 481 | # validate 'n_fft' argument 482 | if n_fft is None: 483 | n_fft = n_samples 484 | 485 | # generate default channel names, if needed 486 | if channel_names is None: 487 | channel_names = [] 488 | for ic in range (0 , n_channels): 489 | icp = ic + 1 490 | channel_names.append( str('Signal-%02d' % icp) ) 491 | 492 | # windowing data 493 | win = scipy.signal.get_window(win_function, n_samples, fftbins=False) 494 | win.shape = (n_samples, 1) 495 | win_rms = np.sqrt(np.sum(np.square(win)) / n_samples) 496 | win_mat = np.tile(win, n_channels) 497 | x = np.multiply(x, win_mat) 498 | 499 | # real FFT with zero padding if n_fft ~= n_samples 500 | Xt = rfft(x, n_fft) 501 | # spectrum scaled by window RMS 502 | Xt = Xt / win_rms 503 | # power spectrum 504 | X_pwr = abs(np.multiply(Xt, np.conj(Xt))) 505 | X_pwr = X_pwr * (1/np.square(n_fft)) 506 | 507 | # adjust for even and odd number of elements 508 | if n_fft % 2 != 0: 509 | # odd case 510 | n_freqs = (n_fft + 1) / 2 511 | # double all frequency components except DC component 512 | X_pwr[1:, :] = X_pwr[1:, :] * 2 513 | 514 | else: 515 | # even case 516 | n_freqs = (n_fft / 2) + 1 517 | # double all frequency components except DC and fs/2 components 518 | X_pwr[1:-1, :] = X_pwr[1:-1, :] * 2 519 | 520 | # frequency axis step 521 | f_delta = (fs / n_fft) 522 | # scale PSD with the frequency step 523 | psd = np.divide(X_pwr, f_delta) 524 | 525 | # frequency axis for spectrum 526 | n_freqs = int(n_freqs) 527 | f_axis = np.asarray(range(0, n_freqs)) * f_delta 528 | 529 | # output 'psd_data' dictionary 530 | psd_data = {} 531 | psd_data['rFFT'] = Xt 532 | psd_data['PSD'] = psd 533 | psd_data['fs'] = fs 534 | psd_data['freq_axis'] = f_axis 535 | psd_data['freq_delta'] = f_delta 536 | psd_data['n_samples'] = n_samples 537 | psd_data['n_fft'] = n_fft 538 | psd_data['win_function'] = win_function 539 | psd_data['channel_names'] = channel_names 540 | 541 | return psd_data 542 | 543 | def irfft_psd(psd_data): 544 | """Compute the inverse PSD for one or a set of REAL signals. 545 | 546 | Parameters 547 | ---------- 548 | psd_data : Structure with PSD data, created with rfft_psd() 549 | 550 | Returns 551 | ------- 552 | x : 1D array with shape (n_samples) or 553 | 2D array with shape (n_samples, n_channels) 554 | 555 | """ 556 | # Load data from PSD structure 557 | rFFT_data = psd_data['rFFT'] 558 | f_ax = psd_data['freq_axis'] 559 | fs = psd_data['fs'] 560 | win_function = psd_data['win_function'] 561 | n_samples = psd_data['n_samples'] 562 | n_channels = rFFT_data.shape[1] 563 | 564 | # Find the number of elements used for the rFFT 565 | if f_ax[-1] < fs/2: 566 | # elements for FFT was odd 567 | n_fft = (len(f_ax) * 2) - 1 568 | elif f_ax[-1] - fs/2 < 1000 * np.finfo(np.float64).eps: 569 | # elements for FFT was even 570 | n_fft = (len(f_ax) - 1) * 2 571 | 572 | # Window RMS 573 | win = scipy.signal.get_window(win_function, n_samples, fftbins=False) 574 | win.shape = (n_samples, 1) 575 | win_rms = np.sqrt(np.sum(np.square(win)) / n_samples) 576 | 577 | # IRFFT 578 | X = rFFT_data * win_rms 579 | x_tmp = irfft(X, n_fft) 580 | 581 | # Keep only n_samples points 582 | x = x_tmp[0 : n_samples + 1, :] 583 | 584 | # Un-Windowing 585 | win_mat = np.tile(win, n_channels) 586 | x = np.divide(x, win_mat) 587 | 588 | return x 589 | 590 | def strfft_spectrogram(x, fs, win_size, win_shift, n_fft=None, win_function='hamming', channel_names=None): 591 | """Compute the Short Time real FFT Spectrogram for one or a set of REAL signals 'x'. 592 | 593 | Parameters 594 | ---------- 595 | x : 1D array with shape (n_samples) or 596 | 2D array with shape (n_samples, n_channels) 597 | fs : Sampling frequency 598 | in Hz 599 | win_size : 600 | Size of the sliding window for STFFF (samples) 601 | win_shift : 602 | Shift between consecutive windows (samples) 603 | n_fft : Number of samples to compute the FFT 604 | (Default = n_samples in array x) 605 | win_function : Window function applied to the signal 606 | (Default 'Hamming') 607 | channel_names : Names of the signals 608 | (Default Signal-XX with XX 1, 2, ... n_channels) 609 | 610 | Returns 611 | ------- 612 | spectrogram_data : Dictionary with Spectrogram data, with the elements: 613 | rFFT_spectrogram 614 | rFFT values for each window (u), scaled by the Window RMS 615 | power_spectrogram : 616 | PSD values for each window (u^2 / Hz) 617 | fs : 618 | Sampling frequency (Hz) 619 | freq_axis : 620 | Frequency axis for rFFT and PSD (Hz) 621 | freq_delta : 622 | Frequency axis step (Hz) 623 | time_axis : 624 | Time axis for rFFT_spectrogram and power_spectrogram (s) 625 | time_delta : 626 | Time axis step (s) 627 | win_size_samples : 628 | Size of the sliding window for STFFF (samples) 629 | win_shift_samples : 630 | Shift between consecutive windows (samples) 631 | n_fft : 632 | Number of elements utilized to perform FFT 633 | win_function : 634 | Window applied to the data in 'x' 635 | n_windows : 636 | Number of ST windows 637 | n_samples : 638 | Number of samples of the signal or signals 'x' 639 | channel_names 640 | Names of channels 641 | 642 | """ 643 | 644 | # input 'x' as 2D matrix [samples, columns] 645 | try: 646 | x.shape[1] 647 | except IndexError: 648 | x = x[:, np.newaxis] 649 | 650 | # number of samples and number of channels 651 | n_samples, n_channels = x.shape 652 | 653 | # validate 'n_fft' argument 654 | if n_fft is None: 655 | n_fft = win_size 656 | 657 | # round win_size and win_shift 658 | win_size = round(win_size) 659 | win_shift = round(win_shift) 660 | 661 | # time axis step for Spectrogram 662 | t_delta = win_shift / fs 663 | 664 | # Create time vector 'time_vct' for signal 'x' 665 | time_vct = np.array(range(0, np.size(x, 0))) / fs 666 | 667 | 668 | # epoch signal or signals 'x' 669 | x_epoched, _ , ix = epoching(x, win_size, win_size - win_shift) 670 | 671 | # time axis for Spectrogram 672 | t_ax = time_vct[ix] 673 | 674 | # spectrogram parameters 675 | n_samples_win, n_channels, n_windows = x_epoched.shape 676 | 677 | # generate default channel names, if needed 678 | if channel_names is None: 679 | channel_names = [] 680 | for ic in range (0 , n_channels): 681 | icp = ic + 1 682 | channel_names.append( str('Signal-%02d' % icp) ) 683 | 684 | # compute PSD per window 685 | for i_window in range(0, n_windows): 686 | # ith epoch of the signal or signals 687 | x_epoch = (x_epoched[:, :, i_window]) 688 | psd_struct = rfft_psd(x_epoch, fs, n_fft, win_function, channel_names) 689 | 690 | # initialize arrays for spectrogram data 691 | if i_window == 0: 692 | # frequency Axis for spectrogram 693 | f_ax = psd_struct['freq_axis'] 694 | # delta Frequency 695 | f_delta = psd_struct['freq_delta'] 696 | # initialize 'rFFT_spectrogram' and 'pwr_spectrogram' 697 | rFFT_spectrogram = np.zeros((n_windows, len(f_ax), n_channels), dtype = complex) 698 | pwr_spectrogram = np.zeros((n_windows, len(f_ax), n_channels)) 699 | 700 | # rFFT data 701 | rFFT_spectrogram[i_window, :, :] = psd_struct['rFFT'] 702 | # power data 703 | pwr_spectrogram[i_window, :, :] = psd_struct['PSD'] 704 | 705 | # scale 'pwr_spectrogram' by number of windows and time delta 706 | pwr_spectrogram = pwr_spectrogram / (n_windows * t_delta) 707 | 708 | 709 | # output 'spectrogram_data' dictionary 710 | spectrogram_data = {} 711 | spectrogram_data['rFFT_spectrogram'] = rFFT_spectrogram 712 | spectrogram_data['power_spectrogram'] = pwr_spectrogram 713 | spectrogram_data['fs'] = fs 714 | spectrogram_data['freq_axis'] = f_ax 715 | spectrogram_data['freq_delta'] = f_delta 716 | spectrogram_data['time_axis'] = t_ax 717 | spectrogram_data['time_delta'] = t_delta 718 | spectrogram_data['win_size_samples'] = win_size 719 | spectrogram_data['win_shift_samples'] = win_shift 720 | spectrogram_data['n_fft'] = n_fft 721 | spectrogram_data['win_function'] = win_function 722 | spectrogram_data['n_windows'] = n_windows 723 | spectrogram_data['n_samples'] = n_samples 724 | spectrogram_data['channel_names'] = channel_names 725 | 726 | return spectrogram_data 727 | 728 | def istrfft_spectrogram(spectrogram_data): 729 | """Compute the inverse STFT spectrogram for one or a set of REAL signals. 730 | 731 | Parameters 732 | ---------- 733 | spectrogram_data : Structure with STFT spectrogram data, created with strfft_spectrogram() 734 | 735 | Returns 736 | ------- 737 | x : 1D array with shape (n_samples) or 738 | 2D array with shape (n_samples, n_channels) 739 | x_epoched = Segments form the signal or set of signals utilized to 740 | create the spectrogram in spectrogram_struct 741 | 742 | """ 743 | # Load data from Spectrogram structure 744 | rFFT_data = spectrogram_data['rFFT_spectrogram'] 745 | win_size = spectrogram_data['win_size_samples'] 746 | win_shift = spectrogram_data['win_shift_samples'] 747 | 748 | # Generate psd_struct, to use irfft_psd() 749 | psd_struct = {} 750 | psd_struct['fs'] = spectrogram_data['fs'] 751 | psd_struct['channel_names'] = spectrogram_data['channel_names'] 752 | psd_struct['freq_axis'] = spectrogram_data['freq_axis'] 753 | psd_struct['win_function'] = spectrogram_data['win_function'] 754 | psd_struct['n_samples'] = win_size 755 | 756 | # Initialize rFFT_slice and x_epoched variables 757 | (n_windows, n_freqs, n_channels) = rFFT_data.shape 758 | rfft_slide = np.zeros((n_freqs, n_channels)) 759 | x_epoched = np.zeros((win_size, n_channels, n_windows)) 760 | 761 | for i_window in range(n_windows): 762 | # rFFT slice from spectrogram 763 | rfft_slide = rFFT_data[i_window, :, :] 764 | # Generate psd_struct, to use irfft_psd() 765 | psd_struct['rFFT'] = rfft_slide 766 | # ifft_psd from the rFFT data recovers the signal or set of signals 'x' 767 | x_tmp = irfft_psd(psd_struct) 768 | x_epoched[:, :, i_window] = x_tmp 769 | 770 | # Merge epoched data 771 | x = iepoching(x_epoched, win_shift); 772 | 773 | return x, x_epoched 774 | 775 | def wavelet_spectrogram(x, fs, n_cycles=6, freq_vct=None, channel_names=None): 776 | """Compute the Spectrogram using the Complex Morlet wavelet for one or a set of REAL signals 'x'. 777 | 778 | Parameters 779 | ---------- 780 | x : 1D array with shape (n_samples) or 781 | 2D array with shape (n_samples, n_channels) 782 | fs : Sampling frequency 783 | in Hz 784 | n : Number of cicles inside the Gaussian curve 785 | (Default 6) 786 | freq_vct : 1D array 787 | with frequencies to compute the CWT (Default = [1 : 1 : fs/2] ) 788 | channel_names : Names of the signals 789 | (Default Signal-XX with XX 1, 2, ... n_channels) 790 | 791 | Returns 792 | ------- 793 | spectrogram_data : Dictionary with Spectrogram data, with the elements: 794 | wavelet_coefficients 795 | Coefficients of the Wavelet transformation (u) 796 | power_spectrogram : 797 | Power spectrogram (u^2 / Hz) 798 | fs : 799 | Sampling frequency (Hz) 800 | freq_axis : 801 | Frequency axis for rFFT and PSD (Hz) 802 | freq_delta : 803 | Frequency axis step (Hz) 804 | time_axis : 805 | Time axis for rFFT_spectrogram and power_spectrogram (s) 806 | time_delta : 807 | Time axis step (s) 808 | n_cycles : 809 | Number of cicles used inside the Gaussian curve 810 | wavelet_kernels : 811 | Wavelet kernels used to obtain the wavelet coefficients 812 | n_samples : 813 | Number of samples of the signal or signals 'x' 814 | channel_names 815 | Names of channels 816 | 817 | """ 818 | # input 'x' as 2D matrix [samples, columns] 819 | try: 820 | x.shape[1] 821 | except IndexError: 822 | x = x[:, np.newaxis] 823 | 824 | # number of samples and number of channels 825 | n_samples, n_channels = x.shape 826 | 827 | # validate 'freq_vct' argument 828 | if freq_vct is None: 829 | freq_vct = np.array(range(1, int(np.floor(fs / 2) + 1))) 830 | 831 | # generate default channel names, if needed 832 | if channel_names is None: 833 | channel_names = [] 834 | for ic in range (0 , n_channels): 835 | icp = ic + 1 836 | channel_names.append( str('Signal-%02d' % icp) ) 837 | 838 | # Time delta 839 | t_delta = 1 / fs 840 | 841 | # Frequency delta 842 | f_delta = freq_vct[1] - freq_vct[0] 843 | 844 | # Create time vector 'time_vct' for signal 'x' 845 | time_vct = np.array(range(0, np.size(x, 0))) / fs 846 | 847 | # Number of samples 848 | n_samples = np.size(x, 0) 849 | 850 | # Wavelet transform 851 | wavelet_coefficients, wavelet_family = cmorlet_wavelet(x, fs, freq_vct, n_cycles) 852 | 853 | # Power from Wavelet coefficients 854 | power_spectrogram = np.square(np.abs(wavelet_coefficients)) 855 | power_spectrogram = power_spectrogram * 2 / (fs * n_samples) 856 | 857 | # output 'spectrogram_data' dictionary 858 | spectrogram_data = {} 859 | spectrogram_data['wavelet_coefficients'] = wavelet_coefficients 860 | spectrogram_data['power_spectrogram'] = power_spectrogram 861 | spectrogram_data['fs'] = fs 862 | spectrogram_data['freq_axis'] = freq_vct 863 | spectrogram_data['freq_delta'] = f_delta 864 | spectrogram_data['time_axis'] = time_vct 865 | spectrogram_data['time_delta'] = t_delta 866 | spectrogram_data['n_cycles'] = n_cycles 867 | spectrogram_data['wavelet_kernels'] = wavelet_family 868 | spectrogram_data['n_samples'] = n_samples 869 | spectrogram_data['channel_names'] = channel_names 870 | 871 | return spectrogram_data 872 | 873 | def iwavelet_spectrogram(spectrogram_data): 874 | """ Compute the inverse CWT Spectrogram for one or a set of REAL signals. 875 | 876 | Parameters 877 | ---------- 878 | spectrogram_data : Structure with CWT Spectrogram data, created with wavelet_spectrogram() 879 | 880 | Returns 881 | ------- 882 | x : 1D array with shape (n_samples) or 883 | 2D array with shape (n_samples, n_channels) 884 | x_epoched = Segments form the signal or set of signals utilized to 885 | create the spectrogram in spectrogram_struct 886 | 887 | """ 888 | 889 | # compute the scaling factor for each wavelet kernel 890 | s = spectrogram_data['n_cycles'] / ( 2 * np.pi * spectrogram_data['freq_axis']) 891 | A = 1. / ((s**2) * np.pi) ** (1./4) 892 | 893 | 894 | x_tmp = np.real(spectrogram_data['wavelet_coefficients']) 895 | 896 | # compute the mean across scaled "filtered" signals 897 | for ix, a in enumerate(A): 898 | x_tmp[:, ix, :] = x_tmp[:, ix, :] / a 899 | 900 | x = np.mean(x_tmp, axis = 1) 901 | 902 | #x = squeeze(mean( bsxfun(@rdivide, real(spectrogram_data.wavelet_coefficients) , A ), 2)); 903 | 904 | return x 905 | 906 | def strfft_modulation_spectrogram(x, fs, win_size, win_shift, fft_factor_y=None, win_function_y='hamming', fft_factor_x=None, win_function_x='hamming', channel_names=None): 907 | """Compute the Modulation Spectrogram using the Complex Morlet wavelet for one or a set of REAL signals 'x'. 908 | 909 | Parameters 910 | ---------- 911 | x : 1D array with shape (n_samples) or 912 | 2D array with shape (n_samples, n_channels) 913 | fs : Sampling frequency 914 | in Hz 915 | win_size : 916 | Size of the sliding window for STFFF (samples) 917 | win_shift : 918 | Shift between consecutive windows (samples) 919 | fft_factor_y : Number of elements to perform the 1st FFT is given as: 920 | n_fft_y = fft_factor_y * n_samples, (default, fft_factor_y = 1) 921 | win_function_y : Window to apply in the 1st FFT 922 | (Default 'Hamming') 923 | fft_factor_x : Number of elements to perform the 2nd FFT is given as: 924 | n_fft_x = fft_factor_x * n_samples, (default, fft_factor_x = 1) 925 | win_function_x : Window to apply in the 2nd rFFT 926 | (Default 'Hamming') 927 | n_fft : Number of samples to compute the FFT 928 | (Default = n_samples in array x) 929 | channel_names : Names of the signals 930 | (Default Signal-XX with XX 1, 2, ... n_channels) 931 | 932 | Returns 933 | ------- 934 | modulation_spectrogram_data : Dictionary with Modulation Spectrogram data, with the elements: 935 | rFFT_modulation_spectrogram 936 | rFFT values for each window (u), scaled by the Window RMS 937 | power_modulation_spectrogram : 938 | Power modulation spectrogram (u^2 / Hz) 939 | fs : 940 | Sampling frequency (Hz) 941 | fs_mod : 942 | Sampling frequency of modulation-frequency (Hz) 943 | freq_axis : 944 | Frequency axis for rFFT and PSD (Hz) 945 | freq_delta : 946 | Frequency axis step (Hz) 947 | freq_mod_axis : 948 | Modulation-frequency axis for rFFT_modspec and pwr_modspec (Hz) 949 | freq_mod_delta : 950 | Modulation-frequency step (Hz) 951 | win_size_samples : 952 | Size of the sliding window for STFFF (samples) 953 | win_shift_samples : 954 | Shift between consecutive windows (samples) 955 | n_fft_y : 956 | Number of elements utilized to perform the 1st FFT 957 | n_fft_x : 958 | Number of elements utilized to perform the 2nd FFT 959 | win_function_y : 960 | Window to apply in the 1st rFFT 961 | win_function_x : 962 | Window to apply in the 2nd rFFT 963 | n_windows : 964 | Number of ST windows 965 | n_samples : 966 | Number of samples of the signal or signals 'x' 967 | spectrogram_data : 968 | Dictionary with Spectrogram data 969 | channel_names : 970 | Names of channels 971 | 972 | """ 973 | # input 'x' as 2D matrix [samples, columns] 974 | try: 975 | x.shape[1] 976 | except IndexError: 977 | x = x[:, np.newaxis] 978 | 979 | # number of samples and number of channels 980 | n_samples, n_channels = x.shape 981 | 982 | # validate 'fft_factor_y' argument 983 | if fft_factor_y is None: 984 | fft_factor_y = 1 985 | 986 | # validate 'fft_factor_x' argument 987 | if fft_factor_x is None: 988 | fft_factor_x = 1 989 | 990 | # number of elements for the 1st FFT 991 | n_fft_y = fft_factor_y * win_size 992 | 993 | 994 | # compute STFFT spectrogram 995 | spectrogram_data = strfft_spectrogram(x, fs, win_size, win_shift, n_fft_y, win_function_y, channel_names) 996 | n_windows, n_freqs, n_channels = spectrogram_data['rFFT_spectrogram'].shape 997 | # Number of elements for the 2nd FFT 998 | n_fft_x = fft_factor_x * n_windows 999 | 1000 | # generate default channel names, if needed 1001 | if channel_names is None: 1002 | channel_names = [] 1003 | for ic in range (0 , n_channels): 1004 | icp = ic + 1 1005 | channel_names.append( str('Signal-%02d' % icp) ) 1006 | 1007 | # modulation sampling frequency 1008 | fs_mod = 1 / (win_shift / fs) 1009 | 1010 | # the AM analysis is made in the Amplitude derived from the Power Spectrogram 1011 | for i_channel in range(0, n_channels): 1012 | # data to generate the Modulation Spectrogram 1013 | spectrogram_1ch = np.sqrt(spectrogram_data['power_spectrogram'][:,:,i_channel]) 1014 | 1015 | # compute 'rfft_psd' on each frequency timeseries 1016 | mod_psd_struct = rfft_psd(spectrogram_1ch, fs_mod, n_fft_x, win_function_x, channel_names ) 1017 | 1018 | if i_channel == 0: 1019 | # modulation frequency axis 1020 | fmod_ax = mod_psd_struct['freq_axis'] 1021 | # modulation frequency delta 1022 | fmod_delta = mod_psd_struct['freq_delta'] 1023 | 1024 | # initialize 'rFFT_modspec' and 'pwr_modspec' 1025 | n_freqsmod = len(fmod_ax) 1026 | rFFT_modspec = np.zeros((n_freqs, n_freqsmod ,n_channels), dtype = complex) 1027 | pwr_modspec = np.zeros((n_freqs, n_freqsmod ,n_channels)) 1028 | 1029 | # rFFT data 1030 | rFFT_modspec[:, :, i_channel] = mod_psd_struct['rFFT'].transpose() 1031 | # power data 1032 | pwr_modspec[:, :, i_channel] = mod_psd_struct['PSD'].transpose() 1033 | 1034 | # scale 'pwr_modspec' by modulation-frequency delta 1035 | pwr_modspec = pwr_modspec / fmod_delta 1036 | 1037 | # output 'modulation_spectrogram_data' structure 1038 | modulation_spectrogram_data = {} 1039 | modulation_spectrogram_data['rFFT_modulation_spectrogram'] = rFFT_modspec 1040 | modulation_spectrogram_data['power_modulation_spectrogram'] = pwr_modspec 1041 | modulation_spectrogram_data['fs'] = fs 1042 | modulation_spectrogram_data['fs_mod'] = fs_mod 1043 | modulation_spectrogram_data['freq_axis'] = spectrogram_data['freq_axis'] 1044 | modulation_spectrogram_data['freq_delta'] = spectrogram_data['freq_delta'] 1045 | modulation_spectrogram_data['freq_mod_axis'] = fmod_ax 1046 | modulation_spectrogram_data['freq_mod_delta'] = fmod_delta 1047 | modulation_spectrogram_data['win_size_samples'] = win_size 1048 | modulation_spectrogram_data['win_shift_samples'] = win_shift 1049 | modulation_spectrogram_data['n_fft_y'] = n_fft_y 1050 | modulation_spectrogram_data['n_fft_x'] = n_fft_x 1051 | modulation_spectrogram_data['win_function_y'] = win_function_y 1052 | modulation_spectrogram_data['win_function_x'] = win_function_x 1053 | modulation_spectrogram_data['n_windows'] = n_windows 1054 | modulation_spectrogram_data['n_samples'] = spectrogram_data['n_samples'] 1055 | modulation_spectrogram_data['spectrogram_data'] = spectrogram_data 1056 | modulation_spectrogram_data['channel_names'] = channel_names 1057 | 1058 | return modulation_spectrogram_data 1059 | 1060 | def istrfft_modulation_spectrogram(modulation_spectrogram_data): 1061 | """ Compute the inverse STFT-based modulation spectrogram for one or a set of REAL signals. 1062 | 1063 | Parameters 1064 | ---------- 1065 | modulation_spectrogram_data : Structure with STFT-based modulation spectrogram data, 1066 | created with strfft_modulation_spectrogram() 1067 | 1068 | Returns 1069 | ------- 1070 | x : 1D array with shape (n_samples) or 1071 | 2D array with shape (n_samples, n_channels) 1072 | 1073 | """ 1074 | # Number of channels from Modspectrogram structure 1075 | n_channels = modulation_spectrogram_data['rFFT_modulation_spectrogram'].shape[2] 1076 | 1077 | # Prepare psd_tmp_data to perform irFFT on Modulation Spectogram 1078 | psd_tmp_data = {} 1079 | psd_tmp_data['freq_axis'] = modulation_spectrogram_data['freq_mod_axis'] 1080 | psd_tmp_data['fs'] = modulation_spectrogram_data['fs_mod'] 1081 | psd_tmp_data['win_function'] = modulation_spectrogram_data['win_function_x'] 1082 | psd_tmp_data['n_samples'] = modulation_spectrogram_data['n_windows'] 1083 | 1084 | 1085 | for i_channel in range(n_channels): 1086 | # Slide with the rFFT coeffients of the 2nd FFT 1087 | psd_tmp_data['rFFT'] = np.transpose(modulation_spectrogram_data['rFFT_modulation_spectrogram'][:,:,i_channel]) 1088 | # Recovers the Square Root of the Power Spectrogram 1089 | sqrt_pwr_spectrogram = irfft_psd(psd_tmp_data) 1090 | # Power Spectrogram 1091 | pwr_spectrogram = sqrt_pwr_spectrogram ** 2 1092 | # Scale Power Spectrogram by (n_windows * time_delta) 1093 | pwr_spectrogram = pwr_spectrogram * modulation_spectrogram_data['spectrogram_data']['n_windows'] * modulation_spectrogram_data['spectrogram_data']['time_delta'] 1094 | # Scale Power Spectrogram by (freq_delta) 1095 | pwr_spectrogram = pwr_spectrogram * modulation_spectrogram_data['spectrogram_data']['freq_delta'] 1096 | # Scale Power Spectrogram by the number of samples used 1097 | pwr_spectrogram = pwr_spectrogram / (1 / modulation_spectrogram_data['spectrogram_data']['n_fft'] ** 2) 1098 | # Divde by 2 all the elements except DC and the Nyquist point (in even case) 1099 | pwr_spectrogram = pwr_spectrogram / 2 1100 | pwr_spectrogram[:, 0] = pwr_spectrogram[:, 0] * 2 1101 | if np.mod(modulation_spectrogram_data['spectrogram_data']['n_fft'], 2) == 0: 1102 | # NFFT was even, then 1103 | pwr_spectrogram[:, -1] = pwr_spectrogram[:, -1] * 2 1104 | spectrogram_abs = np.sqrt(pwr_spectrogram) 1105 | # Recovers the Angle values of the Spectrogram 1106 | spectrogram_angle = np.angle(modulation_spectrogram_data['spectrogram_data']['rFFT_spectrogram'][:,:,i_channel]) 1107 | # Creates the rFFT coefficients of the 1st FFTs 1108 | modulation_spectrogram_data['spectrogram_data']['rFFT_spectrogram'][:,:,i_channel] = spectrogram_abs * np.exp(1j * spectrogram_angle ) 1109 | 1110 | # Recovers the origial signal or set of signals 1111 | x = istrfft_spectrogram(modulation_spectrogram_data['spectrogram_data'])[0] 1112 | 1113 | return x 1114 | 1115 | def wavelet_modulation_spectrogram(x, fs, n_cycles=6, freq_vct=None, fft_factor_x=1, win_function_x='hamming', channel_names=None): 1116 | """Compute the Modulation Spectrogram using the Wavelet for one or a set of REAL signals 'x'. 1117 | 1118 | Parameters 1119 | ---------- 1120 | x : 1D array with shape (n_samples) or 1121 | 2D array with shape (n_samples, n_channels) 1122 | fs : Sampling frequency 1123 | in Hz 1124 | n : Number of cicles inside the Gaussian curve 1125 | (Default 6) 1126 | freq_vct : 1D array 1127 | with frequencies to compute the CWT (Default = [1 : 1 : fs/2] ) 1128 | fft_factor_x : Number of elements to perform the FFT is given as: 1129 | n_fft_x = fft_factor_x * n_samples, (default, fft_factor_x = 1) 1130 | win_function_x : Window to apply in the rFFT 1131 | (Default 'Hamming') 1132 | channel_names : Names of the signals 1133 | (Default Signal-XX with XX 1, 2, ... n_channels) 1134 | 1135 | Returns 1136 | ------- 1137 | modulation_spectrogram_data : Dictionary with Modulation Spectrogram data, with the elements: 1138 | rFFT_modulation_spectrogram 1139 | rFFT values for each window (u), scaled by the Window RMS 1140 | power_modulation_spectrogram : 1141 | Power modulation spectrogram (u^2 / Hz) 1142 | fs : 1143 | Sampling frequency (Hz) 1144 | fs_mod : 1145 | Sampling frequency of modulation-frequency (Hz) 1146 | freq_axis : 1147 | Frequency axis for rFFT and PSD (Hz) 1148 | freq_delta : 1149 | Frequency axis step (Hz) 1150 | freq_mod_axis : 1151 | Modulation-frequency axis for rFFT_modspec and pwr_modspec (Hz) 1152 | freq_mod_delta : 1153 | Modulation-frequency step (Hz) 1154 | n_fft_x : 1155 | Number of elements utilized to perform the FFT 1156 | win_function_x : 1157 | Window to apply in the 2nd rFFT 1158 | n_samples : 1159 | Number of samples of the signal or signals 'x' 1160 | spectrogram_data : 1161 | Dictionary with Spectrogram data 1162 | channel_names : 1163 | Names of channels 1164 | 1165 | """ 1166 | # input 'x' as 2D matrix [samples, columns] 1167 | try: 1168 | x.shape[1] 1169 | except IndexError: 1170 | x = x[:, np.newaxis] 1171 | 1172 | # number of samples and number of channels 1173 | n_samples, n_channels = x.shape 1174 | 1175 | # generate default channel names, if needed 1176 | if channel_names is None: 1177 | channel_names = [] 1178 | for ic in range (0 , n_channels): 1179 | icp = ic + 1 1180 | channel_names.append( str('Signal-%02d' % icp) ) 1181 | 1182 | spectrogram_data = wavelet_spectrogram(x, fs, n_cycles, freq_vct, channel_names) 1183 | n_windows, n_freqs, n_channels = spectrogram_data['wavelet_coefficients'].shape 1184 | 1185 | # number of elements for FFT of the spectrogram 1186 | n_fft_x = fft_factor_x * n_windows 1187 | 1188 | fs_mod = fs 1189 | 1190 | # the AM analysis is made in the Amplitude derived from the Power Spectrogram 1191 | for i_channel in range(0, n_channels): 1192 | # data to generate the Modulation Spectrogram 1193 | spectrogram_1ch = np.sqrt(spectrogram_data['power_spectrogram'][:, :, i_channel]) 1194 | # Compute rfft_psd on each frequency timeseries 1195 | psd_dict = rfft_psd(spectrogram_1ch, fs, n_fft_x) 1196 | 1197 | rfft_result = psd_dict['rFFT'] 1198 | rfft_psd_res = psd_dict['PSD'] 1199 | 1200 | if i_channel == 0: 1201 | # modulation frequency axis 1202 | fmod_ax = psd_dict['freq_axis'] 1203 | # modulation frequency delta 1204 | fmod_delta = psd_dict['freq_delta'] 1205 | n_freqsmod = np.size(fmod_ax) 1206 | # initialize 'rFFT_modspec' and 'pwr_modspec' 1207 | rfft_modspec = np.zeros((n_freqs, n_freqsmod, n_channels), dtype = complex) 1208 | pwr_modspec = np.zeros((n_freqs, n_freqsmod, n_channels)) 1209 | 1210 | # rFFT data 1211 | rfft_modspec[:, :, i_channel] = np.transpose(rfft_result) 1212 | # power data 1213 | pwr_modspec[:, :, i_channel] = np.transpose(rfft_psd_res) 1214 | 1215 | # scale 'pwr_modspec' by modulation-frequency delta 1216 | pwr_modspec = pwr_modspec / fmod_delta 1217 | 1218 | # output 'modulation_spectrogram_data' dictionary 1219 | modulation_spectrogram_data = {} 1220 | modulation_spectrogram_data['rFFT_modulation_spectrogram'] = rfft_modspec 1221 | modulation_spectrogram_data['power_modulation_spectrogram'] = pwr_modspec 1222 | modulation_spectrogram_data['fs'] = fs 1223 | modulation_spectrogram_data['fs_mod'] = fs_mod 1224 | modulation_spectrogram_data['freq_axis'] = spectrogram_data['freq_axis'] 1225 | modulation_spectrogram_data['freq_delta'] = spectrogram_data['freq_delta'] 1226 | modulation_spectrogram_data['freq_mod_axis'] = fmod_ax 1227 | modulation_spectrogram_data['freq_mod_delta'] = fmod_delta 1228 | modulation_spectrogram_data['n_fft_x'] = n_fft_x 1229 | modulation_spectrogram_data['win_function_x'] = win_function_x 1230 | modulation_spectrogram_data['n_samples'] = spectrogram_data['n_samples'] 1231 | modulation_spectrogram_data['spectrogram_data'] = spectrogram_data 1232 | modulation_spectrogram_data['channel_names'] = channel_names 1233 | 1234 | return modulation_spectrogram_data 1235 | 1236 | def iwavelet_modulation_spectrogram(modulation_spectrogram_data): 1237 | """ Compute the inverse CWT-based modulation spectrogram for one or a set of REAL signals. 1238 | 1239 | Parameters 1240 | ---------- 1241 | modulation_spectrogram_data : Structure with CWT-based modulation spectrogram data, 1242 | created with wavelet_modulation_spectrogram() 1243 | 1244 | Returns 1245 | ------- 1246 | x : 1D array with shape (n_samples) or 1247 | 2D array with shape (n_samples, n_channels) 1248 | 1249 | """ 1250 | # Number of channels from Modspectrogram structure 1251 | n_channels = modulation_spectrogram_data['rFFT_modulation_spectrogram'].shape[2] 1252 | 1253 | # Prepare psd_tmp_data to perform irFFT on Modulation Spectogram 1254 | psd_tmp_data = {} 1255 | psd_tmp_data['freq_axis'] = modulation_spectrogram_data['freq_mod_axis'] 1256 | psd_tmp_data['fs'] = modulation_spectrogram_data['fs_mod'] 1257 | psd_tmp_data['win_function'] = modulation_spectrogram_data['win_function_x'] 1258 | psd_tmp_data['n_samples'] = modulation_spectrogram_data['n_samples'] 1259 | 1260 | 1261 | for i_channel in range(n_channels): 1262 | # Slide with the rFFT coeffients of the 2nd FFT 1263 | psd_tmp_data['rFFT'] = np.transpose(modulation_spectrogram_data['rFFT_modulation_spectrogram'][:,:,i_channel]) 1264 | # Recovers the Square Root of the Power Spectrogram 1265 | sqrt_pwr_spectrogram = irfft_psd(psd_tmp_data) 1266 | 1267 | # Recovers the Magnitude of the Wavelet Coefficents 1268 | pwr_spectrogram = sqrt_pwr_spectrogram ** 2 1269 | pwr_spectrogram = pwr_spectrogram * modulation_spectrogram_data['fs_mod'] * modulation_spectrogram_data['n_samples'] 1270 | pwr_spectrogram = pwr_spectrogram / 2 1271 | spectrogram_abs = np.sqrt(pwr_spectrogram) 1272 | 1273 | # Recovers the Angle values of the Spectrogram 1274 | spectrogram_angle = np.angle(modulation_spectrogram_data['spectrogram_data']['wavelet_coefficients'][:,:,i_channel]) 1275 | 1276 | # Creates the rFFT coefficients of the 1st FFTs 1277 | modulation_spectrogram_data['spectrogram_data']['wavelet_coefficients'][:,:,i_channel] = spectrogram_abs * np.exp(1j * spectrogram_angle ) 1278 | 1279 | # Recovers the origial signal or set of signals 1280 | x = iwavelet_spectrogram(modulation_spectrogram_data['spectrogram_data']) 1281 | 1282 | return x 1283 | 1284 | def plot_spectrogram_data(spectrogram_data, ix=None, t_range=None, f_range=None, c_range=None, c_map='viridis'): 1285 | """ Plot the Power Spectrogram related to the `spectrogram_data` 1286 | 1287 | Parameters 1288 | ---------- 1289 | spectrogram_data : 1290 | Dictionary with Spectrogram data 1291 | ix : Index of the signal (channel) to plot 1292 | (Default, all the channels, a new figure for each) 1293 | t_range : Time range 1294 | (Default [minimum time, maximum time]) 1295 | f_range : Frequency range 1296 | (Default [minimum frequency, maximum frequency]) 1297 | c_range : Color (power) range 1298 | (Default [mean power, maximum power]) 1299 | c_map : Colot Map 1300 | (Default viridis) 1301 | 1302 | Returns 1303 | ------- 1304 | If only a plot is requested, it is plotted in the existen axes (created if needed) 1305 | If many plots are requested, a new figure is created for each plot 1306 | 1307 | """ 1308 | 1309 | def plot_one_spectrogram(ax, X_pwr, t_ax, f_ax, title_str, t_range, f_range, c_range, c_map): 1310 | """ 1311 | Plots ONLY ONE Spectrogram 1312 | """ 1313 | T, F = np.meshgrid(t_ax, f_ax) 1314 | X_plot = 10 * np.log10(X_pwr[:,:].transpose() + np.finfo(float).eps) 1315 | pmesh = plt.pcolormesh(T,F,X_plot, cmap=c_map) 1316 | 1317 | # Major and Minor ticks 1318 | ax = plt.gca() 1319 | ax.xaxis.set_major_locator(ticker.AutoLocator()) 1320 | ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) 1321 | ax.yaxis.set_major_locator(ticker.AutoLocator()) 1322 | ax.yaxis.set_minor_locator(ticker.AutoMinorLocator()) 1323 | 1324 | plt.xlabel('fime (s)') 1325 | plt.ylabel('frequency (Hz)') 1326 | 1327 | 1328 | if t_range is not None: 1329 | xlim = t_range 1330 | else: 1331 | xlim = t_ax 1332 | 1333 | if f_range is not None: 1334 | ylim = f_range 1335 | else: 1336 | ylim = f_ax 1337 | 1338 | # set the limits of the plot to the limits of the data 1339 | plt.axis([xlim.min(), xlim.max(), ylim.min(), ylim.max()]) 1340 | 1341 | if c_range is not None: 1342 | clim = c_range 1343 | else: 1344 | clim = np.array([np.mean(X_plot), np.amax(X_plot)]) 1345 | 1346 | pmesh.set_clim(vmin=clim[0], vmax=clim[1]) 1347 | 1348 | plt.colorbar() 1349 | plt.title(title_str) 1350 | plt.draw() 1351 | 1352 | 1353 | # validate 'ix' argument 1354 | if ix is None: 1355 | ix = range(0, spectrogram_data['power_spectrogram'].shape[2]) 1356 | elif np.isscalar(ix): 1357 | ix = np.array([ix]) 1358 | 1359 | # Check if ix has ONLY one element 1360 | if len(ix) == 1: 1361 | new_figure = False 1362 | # Retrieve Current Axes handle from the Current Figure, if there is not 1363 | # Current Figure, it's generated here 1364 | ax = plt.gca() 1365 | else: 1366 | new_figure = True 1367 | 1368 | for i_channel in ix: 1369 | if new_figure: 1370 | plt.figure() 1371 | ax = plt.gca() 1372 | plot_one_spectrogram(ax, 1373 | spectrogram_data['power_spectrogram'][:, :, i_channel], 1374 | spectrogram_data['time_axis'], 1375 | spectrogram_data['freq_axis'], 1376 | spectrogram_data['channel_names'][i_channel], 1377 | t_range, f_range, c_range, c_map) 1378 | 1379 | 1380 | 1381 | def plot_modulation_spectrogram_data(modulation_spectrogram_data, ix=None, f_range=None, modf_range=None, c_range=None, c_map='viridis'): 1382 | """ Plot the Power Modulation Spectrogram related to the `modulation_spectrogram_data` 1383 | 1384 | Parameters 1385 | ---------- 1386 | modulation_spectrogram_data : 1387 | Dictionary with Modulation Spectrogram data 1388 | ix : Index of the signal (channel) to plot 1389 | (Default, all the channels, a new figure for each) 1390 | f_range : Frequency range 1391 | (Default [minimum frequency, maximum frequency]) 1392 | fm_range : Modulation frequency range 1393 | (Default [minimum mod_frequency, maximum mod_frequency]) 1394 | c_range : Color (power) range 1395 | (Default [mean power, maximum power]) 1396 | c_map : Colot Map 1397 | (Default viridis) 1398 | 1399 | Returns 1400 | ------- 1401 | If only a plot is requested, it is plotted in the existen axes (created if needed) 1402 | If many plots are requested, a new figure is created for each plot 1403 | 1404 | """ 1405 | 1406 | def plot_one_modulation_spectrogram(ax, X_pwr, f_ax, modf_ax, title_str, f_range, modf_range, c_range, c_map): 1407 | """ 1408 | Plots ONLY ONE Modulation Spectrogram 1409 | """ 1410 | MF, F = np.meshgrid(modf_ax, f_ax) 1411 | X_plot = 10 * np.log10(X_pwr[:,:] + np.finfo(float).eps) 1412 | pmesh = plt.pcolormesh(MF, F, X_plot, cmap =c_map) 1413 | 1414 | # Major and Minor ticks 1415 | ax = plt.gca() 1416 | ax.xaxis.set_major_locator(ticker.AutoLocator()) 1417 | ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) 1418 | ax.yaxis.set_major_locator(ticker.AutoLocator()) 1419 | ax.yaxis.set_minor_locator(ticker.AutoMinorLocator()) 1420 | 1421 | plt.xlabel('modulation frequency (Hz)') 1422 | plt.ylabel('conventional frequency (Hz)') 1423 | 1424 | 1425 | if modf_range is not None: 1426 | xlim = modf_range 1427 | else: 1428 | xlim = modf_ax 1429 | 1430 | if f_range is not None: 1431 | ylim = f_range 1432 | else: 1433 | ylim = f_ax 1434 | 1435 | # set the limits of the plot to the limits of the data 1436 | plt.axis([xlim.min(), xlim.max(), ylim.min(), ylim.max()]) 1437 | 1438 | if c_range is not None: 1439 | clim = c_range 1440 | else: 1441 | clim = np.array([np.mean(X_plot), np.amax(X_plot)]) 1442 | 1443 | pmesh.set_clim(vmin=clim[0], vmax=clim[1]) 1444 | 1445 | plt.colorbar() 1446 | plt.title(title_str) 1447 | plt.draw() 1448 | 1449 | # validate 'ix' argument 1450 | if ix is None: 1451 | ix = range(0, modulation_spectrogram_data['power_modulation_spectrogram'].shape[2]) 1452 | elif np.isscalar(ix): 1453 | ix = np.array([ix]) 1454 | 1455 | # Check if ix has ONLY one element 1456 | if len(ix) == 1: 1457 | new_figure = False 1458 | # Retrieve Current Axes handle from the Current Figure, if there is not 1459 | # Current Figure, it's generated here 1460 | ax = plt.gca() 1461 | else: 1462 | new_figure = True 1463 | 1464 | for i_channel in ix: 1465 | if new_figure: 1466 | plt.figure() 1467 | ax = plt.gca() 1468 | plot_one_modulation_spectrogram(ax, 1469 | modulation_spectrogram_data['power_modulation_spectrogram'][:, :, i_channel], 1470 | modulation_spectrogram_data['freq_axis'], 1471 | modulation_spectrogram_data['freq_mod_axis'], 1472 | modulation_spectrogram_data['channel_names'][i_channel], 1473 | f_range, modf_range, c_range, c_map) 1474 | 1475 | 1476 | def plot_psd_data(psd_data, ix=None, p_range=None, f_range=None): 1477 | """ Plot the PSD related to the `psd_data` 1478 | 1479 | Parameters 1480 | ---------- 1481 | psd_data : 1482 | Dictionary with PSD data 1483 | ix : Index of the signal (channel) to plot 1484 | (Default, all the channels, a new figure for each) 1485 | p_range : Power range 1486 | (Default [minimum power, maximum power]) 1487 | f_range : Frequency range 1488 | (Default [minimum frequency, maximum frequency]) 1489 | 1490 | Returns 1491 | ------- 1492 | If only a plot is requested, it is plotted in the existen axes (created if needed) 1493 | If many plots are requested, a new figure is created for each plot 1494 | 1495 | """ 1496 | 1497 | def plot_one_psd(ax, X_pwr, f_ax, title_str, p_range, f_range): 1498 | """ 1499 | Plots ONLY ONE PSD 1500 | """ 1501 | X_plot = 10 * np.log10(X_pwr + np.finfo(float).eps) 1502 | plt.plot(f_ax, X_plot) 1503 | 1504 | # Major and Minor ticks 1505 | ax = plt.gca() 1506 | ax.xaxis.set_major_locator(ticker.AutoLocator()) 1507 | ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) 1508 | ax.yaxis.set_major_locator(ticker.AutoLocator()) 1509 | ax.yaxis.set_minor_locator(ticker.AutoMinorLocator()) 1510 | 1511 | plt.xlabel('frequency (Hz)') 1512 | plt.ylabel('power (dB/Hz)') 1513 | 1514 | 1515 | if f_range is not None: 1516 | xlim = f_range 1517 | else: 1518 | xlim = f_ax 1519 | 1520 | if p_range is not None: 1521 | ylim = p_range 1522 | else: 1523 | ylim = X_plot 1524 | 1525 | # set the limits of the plot to the limits of the data 1526 | plt.axis([xlim.min(), xlim.max(), ylim.min(), ylim.max()]) 1527 | 1528 | plt.title(title_str) 1529 | plt.draw() 1530 | 1531 | # validate 'ix' argument 1532 | if ix is None: 1533 | ix = range(0, psd_data['PSD'].shape[1]) 1534 | elif np.isscalar(ix): 1535 | ix = np.array([ix]) 1536 | 1537 | # Check if ix has ONLY one element 1538 | if len(ix) == 1: 1539 | new_figure = False 1540 | # Retrieve Current Axes handle from the Current Figure, if there is not 1541 | # Current Figure, it's generated here 1542 | ax = plt.gca() 1543 | else: 1544 | new_figure = True 1545 | 1546 | for i_channel in ix: 1547 | if new_figure: 1548 | plt.figure() 1549 | ax = plt.gca() 1550 | plot_one_psd(ax, 1551 | psd_data['PSD'][:, i_channel], 1552 | psd_data['freq_axis'], 1553 | psd_data['channel_names'][i_channel], 1554 | p_range, f_range) 1555 | 1556 | 1557 | def plot_signal(x, fs, name=None): 1558 | """Behaves as matplotlib.pyplot.plot(x) but X axis is definded by `fs` [Hz] 1559 | 1560 | Parameters 1561 | ---------- 1562 | x : 1563 | 1D or 2D Signals as column vectors 1564 | fs : 1565 | Sampling frequency in Hz 1566 | name : 1567 | Name of the signal (Default 'Signal-01') 1568 | """ 1569 | 1570 | # Create time vector 1571 | time_vector = np.arange(x.shape[0])/fs 1572 | 1573 | plt.plot(time_vector,x) 1574 | plt.xlabel('time (s)') 1575 | plt.xlim([time_vector.min(), time_vector.max()]) 1576 | 1577 | if name is None: 1578 | name = 'Signal-01' 1579 | 1580 | plt.title(name) 1581 | plt.draw() 1582 | 1583 | if __name__ == '__main__': 1584 | 1585 | # Example data 1586 | fs = 256 1587 | t_5s = np.arange(20*fs)/fs 1588 | freqs = np.arange(1,101) 1589 | x = np.asarray([np.sin(8*2*np.pi*t_5s), np.sin(25*2*np.pi*t_5s)]) 1590 | 1591 | x = np.transpose(x) 1592 | # x is composed by two signals: 1593 | # 1) a 8 Hz sine wave 1594 | # 2) a 25 hz sine wave 1595 | 1596 | # Compute modulation spectrogram with CWT 1597 | w = wavelet_modulation_spectrogram(x, fs) 1598 | 1599 | # Compute modulation spectrogram with STFT 1600 | f = strfft_modulation_spectrogram(x, fs, 1*fs, 0.5*fs) 1601 | 1602 | plot_modulation_spectrogram_data(w) 1603 | plot_spectrogram_data(w['spectrogram_data']) 1604 | 1605 | plot_modulation_spectrogram_data(f, c_map='jet') 1606 | plot_spectrogram_data(f['spectrogram_data'], c_map='jet') 1607 | -------------------------------------------------------------------------------- /am_analysis/explore_stfft_ama_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | function explore_stfft_ama_gui(X, fs, Names, c_map) 5 | Analysis of a Signal in Frequency-Frequency Domain 6 | Time -> Time-Frequency transformation performed with STFFT 7 | 8 | INPUTS: 9 | X Real-valued column-vector signal or set of signals [n_samples, n_channels] 10 | fs Sampling frequency (Hz) 11 | Optional: 12 | Names (Optional) Name of the signal(s), List of Strings 13 | c_map (Optional) Colormap, Default 'viridis' 14 | 15 | """ 16 | 17 | import sys 18 | import numpy as np 19 | import matplotlib.pyplot as plt 20 | from matplotlib.widgets import TextBox, Button 21 | 22 | 23 | from . import am_analysis as ama 24 | 25 | def press(event): 26 | global ix_segment 27 | global ix_channel 28 | global fig 29 | global cid 30 | 31 | #print('press', event.key) 32 | sys.stdout.flush() 33 | if event.key == 'left': #Left arrow: Previous Segment 34 | ix_segment = ix_segment - 1 35 | update_plots() 36 | elif event.key == 'right': #Right arrow: Next Segment 37 | ix_segment = ix_segment + 1 38 | update_plots() 39 | elif event.key == 'up': #Up arrow: Previous Channel 40 | ix_channel = ix_channel - 1 41 | first_run() 42 | elif event.key == 'down': #Down arrow: Next Channel 43 | ix_channel = ix_channel + 1 44 | first_run() 45 | elif event.key == 'a': # A: Back 5 Segments 46 | ix_segment = ix_segment - 1 47 | update_plots() 48 | elif event.key == 'd': # D: Advance 5 Segments 49 | ix_segment = ix_segment + 1 50 | update_plots() 51 | elif event.key == 'w': # W: Previous 5 Channels 52 | ix_channel = ix_channel - 1 53 | first_run() 54 | elif event.key == 's': # S: Advance 5 Channels 55 | ix_channel = ix_channel + 1 56 | first_run() 57 | elif event.key == 'u': # U: Update parameters 58 | fig.canvas.mpl_disconnect(cid) 59 | create_parameter_gui() 60 | elif event.key == 'escape': 61 | fig.canvas.mpl_disconnect(cid) 62 | plt.close(fig) 63 | 64 | return 65 | 66 | def first_run(): 67 | global ix_segment 68 | global ix_channel 69 | global n_segments 70 | global x_segments 71 | global win_size_smp 72 | global win_shft_smp 73 | global x_probe 74 | global x_spectrogram 75 | global fs 76 | global X 77 | global parameters 78 | global channel_names 79 | 80 | ix_segment = 0 81 | print('Computing full-signal spectrogram...') 82 | 83 | # constrain ix_channel to [1 : n_channels] 84 | ix_channel = np.maximum(0, ix_channel) 85 | ix_channel = np.minimum(n_channels-1, ix_channel) 86 | 87 | # STFFT modulation spectrogram parameters in samples 88 | win_size_smp = round(parameters['win_size_sec'] * fs) # (samples) 89 | win_shft_smp = round(parameters['win_shft_sec'] * fs) # (samples) 90 | seg_size_smp = round(parameters['seg_size_sec'] * fs) # (samples) 91 | seg_shft_smp = round(parameters['seg_shft_sec'] * fs) # (samples) 92 | 93 | # signal for analysis 94 | x_probe = X[:, ix_channel] 95 | 96 | # segment of signal under analysis 97 | x_segments, _, _= ama.epoching(x_probe, seg_size_smp, seg_size_smp - seg_shft_smp) 98 | n_segments = x_segments.shape[2] 99 | 100 | # compute and plot complete spectrogram 101 | x_spectrogram = ama.strfft_spectrogram(x_probe, fs, win_size_smp, win_shft_smp, channel_names=[channel_names[ix_channel]]) 102 | 103 | update_plots() 104 | return 105 | 106 | def create_parameter_gui(): 107 | global fig2 108 | global boxes 109 | global parameters 110 | # new figure for parameters 111 | fig2, ax2 = plt.subplots() 112 | plt.axis('off') 113 | plt.subplots_adjust(top=0.5, left=0.1, right=0.2, bottom=0.4) 114 | 115 | axbox = plt.axes([0.4, 0.85, 0.2, 0.075]) 116 | text_box0 = TextBox(axbox, 'Segment (seconds)', str(parameters['seg_size_sec'])) 117 | axbox = plt.axes([0.4, 0.75, 0.2, 0.075]) 118 | text_box1 = TextBox(axbox, 'Segment shift (seconds)', str(parameters['seg_shft_sec'])) 119 | axbox = plt.axes([0.4, 0.65, 0.2, 0.075]) 120 | text_box2 = TextBox(axbox, 'Window size (seconds)', str(parameters['win_size_sec'])) 121 | axbox = plt.axes([0.4, 0.55, 0.2, 0.075]) 122 | text_box3 = TextBox(axbox, 'Window shift (seconds)', str(parameters['win_shft_sec'])) 123 | axbox = plt.axes([0.4, 0.45, 0.2, 0.075]) 124 | text_box4 = TextBox(axbox, 'Freq Conv. min Max (Hz)', str(parameters['freq_range']).strip('[').strip(']') ) 125 | axbox = plt.axes([0.4, 0.35, 0.2, 0.075]) 126 | text_box5 = TextBox(axbox, 'Spectr Pwr min Max (dB)', str(parameters['freq_color']).strip('[').strip(']')) 127 | axbox = plt.axes([0.4, 0.25, 0.2, 0.075]) 128 | text_box6 = TextBox(axbox, 'Freq Mod. min Max (Hz)', str(parameters['mfreq_range']).strip('[').strip(']')) 129 | axbox = plt.axes([0.4, 0.15, 0.2, 0.075]) 130 | text_box7 = TextBox(axbox, 'ModSpec Pwr min Max (dB)', str(parameters['mfreq_color']).strip('[').strip(']')) 131 | 132 | axbox = plt.axes([0.4, 0.05, 0.2, 0.075]) 133 | ok_button = Button(axbox, 'OK') 134 | 135 | boxes = [text_box0, text_box1, text_box2, text_box3, 136 | text_box4, text_box5, text_box6, text_box7, ok_button] 137 | 138 | ok_button.on_clicked(submit) 139 | return 140 | 141 | def submit(text): 142 | global fig2 143 | global fig 144 | global cid 145 | plt.close(fig2) 146 | update_parameters() 147 | cid = fig.canvas.mpl_connect('key_press_event', press) 148 | 149 | def update_parameters(): 150 | global boxes 151 | global parameters 152 | # Pop reference to Button 153 | boxes.pop(-1) 154 | parameters['seg_size_sec'] = float(boxes[0].text) 155 | parameters['seg_shft_sec'] = float(boxes[1].text) 156 | parameters['win_size_sec'] = float(boxes[2].text) 157 | parameters['win_shft_sec'] = float(boxes[3].text) 158 | if boxes[4].text == 'None': 159 | parameters['freq_range'] = None 160 | else: 161 | parameters['freq_range'] = np.fromstring(boxes[4].text, sep=' ') 162 | if boxes[5].text == 'None': 163 | parameters['freq_color'] = None 164 | else: 165 | parameters['freq_color'] = np.fromstring(boxes[5].text, sep=' ') 166 | if boxes[6].text == 'None': 167 | parameters['mfreq_range'] = None 168 | else: 169 | parameters['mfreq_range'] = np.fromstring(boxes[6].text, sep=' ') 170 | if boxes[7].text == 'None': 171 | parameters['mfreq_color'] = None 172 | else: 173 | parameters['mfreq_color'] = np.fromstring(boxes[7].text, sep=' ') 174 | 175 | first_run() 176 | 177 | return 178 | 179 | 180 | def update_plots(): 181 | global ix_segment 182 | global x_segments 183 | global win_size_smp 184 | global win_shft_smp 185 | global ix_channel 186 | global x_probe 187 | global x_spectrogram 188 | global fig 189 | global fs 190 | global parameters 191 | global gc_map 192 | global channel_names 193 | 194 | fig.clear() 195 | 196 | # constrain ix_segment to [1 : n_segments] 197 | ix_segment = max([0, ix_segment]) 198 | ix_segment = min([n_segments, ix_segment]) 199 | 200 | # select segment 201 | x = x_segments[:, :, ix_segment] 202 | 203 | # compute and plot Modulation Spectrogram 204 | print('Computing modulation spectrogram...') 205 | x_stft_modspec = ama.strfft_modulation_spectrogram(x, fs, win_size_smp, win_shft_smp, fft_factor_y=2, fft_factor_x=2, channel_names=[channel_names[ix_channel]]) 206 | plt.subplot(4,2,(6,8)) 207 | ama.plot_modulation_spectrogram_data(x_stft_modspec, f_range=parameters['freq_range'], modf_range=parameters['mfreq_range'], c_range=parameters['mfreq_color'], c_map=gc_map) 208 | 209 | # plot time series for segment 210 | plt.subplot(4,2,5) 211 | ama.plot_signal(x, fs, channel_names[ix_channel]) 212 | plt.colorbar() 213 | time_lim = plt.xlim() 214 | 215 | # plot spectrogram for segment 216 | plt.subplot(4,2,7) 217 | ama.plot_spectrogram_data(x_stft_modspec['spectrogram_data'], f_range=parameters['freq_range'], c_range=parameters['freq_color'], c_map=gc_map ) 218 | plt.xlim(time_lim) 219 | 220 | # plot full signal 221 | h_ts = plt.subplot(4,2,(1,2)) 222 | ama.plot_signal(x_probe, fs, channel_names[ix_channel]) 223 | plt.colorbar() 224 | time_lim = plt.xlim() 225 | 226 | # plot spectrogram for full signal 227 | h_tf = plt.subplot(4,2,(3,4)) 228 | ama.plot_spectrogram_data(x_spectrogram, f_range=parameters['freq_range'], c_range=parameters['freq_color'], c_map=gc_map ) 229 | plt.xlim(time_lim) 230 | 231 | # highlight area under analysis in time series 232 | seg_ini_sec = (ix_segment ) * parameters['seg_shft_sec'] 233 | seg_end_sec = seg_ini_sec + parameters['seg_size_sec'] 234 | 235 | plt.subplot(h_ts) 236 | varea([seg_ini_sec, seg_end_sec ],'r',0.4) 237 | 238 | # highlight area under analysis in Spectrogram 239 | plt.subplot(h_tf) 240 | varea([seg_ini_sec, seg_end_sec ],'r',0.4) 241 | 242 | print('done!') 243 | 244 | # display information about analysis 245 | print('signal name : %s' % channel_names[ix_channel] ) 246 | print('segment size (seconds): %0.3f' % parameters['seg_size_sec']) 247 | print('segment shift (seconds): %0.3f' % parameters['seg_shft_sec']) 248 | print('segment position (sec): %0.3f' % seg_ini_sec) 249 | print('window size (seconds): %0.3f' % parameters['win_size_sec']) 250 | print('window shift (seconds): %0.3f' % parameters['win_shft_sec']) 251 | print('windows per segment : %d'% x_stft_modspec['n_windows']) 252 | 253 | fig.canvas.draw() 254 | plt.show() 255 | return 256 | 257 | 258 | def varea(xlims, color_str, alpha_v=0.2): 259 | ax = plt.gca() 260 | ylims = ax.get_ylim() 261 | plt.fill((xlims[0], xlims[0], xlims[1], xlims[1]), 262 | (ylims[0], ylims[1], ylims[1], ylims[0]), 263 | color_str, alpha=alpha_v) 264 | return 265 | 266 | 267 | 268 | def explore_stfft_ama_gui(x, fs_arg, channel_names_arg = None, c_map = 'viridis'): 269 | 270 | # Global variables 271 | global ix_channel 272 | global ix_segment 273 | global n_channels 274 | global n_segments 275 | global x_segments 276 | global cid 277 | global fig 278 | 279 | global parameters 280 | global gc_map 281 | 282 | global win_size_smp 283 | global win_shft_smp 284 | 285 | global X 286 | global name 287 | global fs 288 | global channel_names 289 | 290 | fs = fs_arg 291 | channel_names = channel_names_arg 292 | X = x 293 | gc_map = c_map 294 | 295 | # input 'x' as 2D matrix [samples, columns] 296 | try: 297 | X.shape[1] 298 | except IndexError: 299 | X = X[:, np.newaxis] 300 | 301 | # number of channels 302 | n_channels = X.shape[1] 303 | 304 | if type(channel_names) == str and n_channels == 1: 305 | channel_names = [channel_names] 306 | # generate default channel names, if needed 307 | if channel_names is None or len(channel_names) != n_channels: 308 | channel_names = [] 309 | for ic in range (0 , n_channels): 310 | icp = ic + 1 311 | channel_names.append( str('Signal-%02d' % icp) ) 312 | 313 | 314 | #% Amplitude Modulation Analysis 315 | # Default Modulation Analysis parameters 316 | parameters = {} 317 | parameters['seg_size_sec'] = 8.0 # segment of signal to compute the Modulation Spectrogram (seconds) 318 | parameters['seg_shft_sec'] = 8.0 # shift between consecutive segments (seconds) 319 | parameters['win_size_sec'] = 0.5 # window length for the STFFT 320 | parameters['win_shft_sec'] = 0.02 # shift between consecutive windows (seconds) 321 | parameters['freq_range'] = None # limits [min, max] for the conventional frequency axis (Hz) 322 | parameters['mfreq_range'] = None # limits [min, max] for the modulation frequency axis (Hz) 323 | parameters['freq_color'] = None # limits [min, max] for the power in Spectrogram (dB) 324 | parameters['mfreq_color'] = None # limits [min, max] for the power in Modulation Spectrogram (dB) 325 | 326 | # initial channel and segment 327 | ix_channel = 0 328 | ix_segment = 0 329 | 330 | # other variables 331 | n_segments = None 332 | 333 | x_segments = None 334 | name = None 335 | win_size_smp = None 336 | win_shft_smp = None 337 | 338 | # Live GUI 339 | fig = plt.figure() 340 | first_run() 341 | cid = fig.canvas.mpl_connect('key_press_event', press) 342 | 343 | -------------------------------------------------------------------------------- /am_analysis/explore_wavelet_ama_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | function explore_wavelet_ama_gui(X, fs, Names, c_map) 5 | Analysis of a Signal in Frequency-Frequency Domain 6 | Time -> Time-Frequency transformation performed with Wavelet Transform (Complex Morlet) 7 | 8 | INPUTS: 9 | X Real-valued column-vector signal or set of signals [n_samples, n_channels] 10 | fs Sampling frequency (Hz) 11 | Optional: 12 | Names (Optional) Name of the signal(s), List of Strings 13 | c_map (Optional) Colormap, Default 'viridis' 14 | 15 | """ 16 | 17 | import sys 18 | import numpy as np 19 | import matplotlib.pyplot as plt 20 | from matplotlib.widgets import TextBox, Button 21 | 22 | 23 | from . import am_analysis as ama 24 | 25 | def press(event): 26 | global ix_segment 27 | global ix_channel 28 | global fig 29 | global cid 30 | 31 | #print('press', event.key) 32 | sys.stdout.flush() 33 | if event.key == 'left': #Left arrow: Previous Segment 34 | ix_segment = ix_segment - 1 35 | update_plots() 36 | elif event.key == 'right': #Right arrow: Next Segment 37 | ix_segment = ix_segment + 1 38 | update_plots() 39 | elif event.key == 'up': #Up arrow: Previous Channel 40 | ix_channel = ix_channel - 1 41 | first_run() 42 | elif event.key == 'down': #Down arrow: Next Channel 43 | ix_channel = ix_channel + 1 44 | first_run() 45 | elif event.key == 'a': # A: Back 5 Segments 46 | ix_segment = ix_segment - 1 47 | update_plots() 48 | elif event.key == 'd': # D: Advance 5 Segments 49 | ix_segment = ix_segment + 1 50 | update_plots() 51 | elif event.key == 'w': # W: Previous 5 Channels 52 | ix_channel = ix_channel - 1 53 | first_run() 54 | elif event.key == 's': # S: Advance 5 Channels 55 | ix_channel = ix_channel + 1 56 | first_run() 57 | elif event.key == 'u': # U: Update parameters 58 | fig.canvas.mpl_disconnect(cid) 59 | create_parameter_gui() 60 | elif event.key == 'escape': 61 | fig.canvas.mpl_disconnect(cid) 62 | plt.close(fig) 63 | 64 | return 65 | 66 | def first_run(): 67 | global ix_segment 68 | global ix_channel 69 | global n_segments 70 | global x_segments 71 | global n_cycles 72 | global x_probe 73 | global x_spectrogram 74 | global channel_names 75 | global fs 76 | global X 77 | global parameters 78 | 79 | ix_segment = 0 80 | print('Computing full-signal spectrogram...') 81 | 82 | # constrain ix_channel to [1 : n_channels] 83 | ix_channel = np.maximum(0, ix_channel) 84 | ix_channel = np.minimum(n_channels-1, ix_channel) 85 | 86 | # Wavelet modulation spectrogram parameters in samples 87 | seg_size_smp = round(parameters['seg_size_sec'] * fs) # (samples) 88 | seg_shft_smp = round(parameters['seg_shft_sec'] * fs) # (samples) 89 | 90 | n_cycles = round(parameters['n_cycles']) 91 | 92 | # signal for analysis 93 | x_probe = X[:, ix_channel] 94 | 95 | # segment of signal under analysis 96 | x_segments, _, _= ama.epoching(x_probe, seg_size_smp, seg_size_smp - seg_shft_smp) 97 | n_segments = x_segments.shape[2] 98 | 99 | # compute and plot complete spectrogram 100 | x_spectrogram = ama.wavelet_spectrogram(x_probe, fs, n_cycles, channel_names=[channel_names[ix_channel]]) 101 | 102 | update_plots() 103 | return 104 | 105 | def create_parameter_gui(): 106 | global fig2 107 | global boxes 108 | global parameters 109 | # new figure for parameters 110 | fig2, ax2 = plt.subplots() 111 | plt.axis('off') 112 | plt.subplots_adjust(top=0.5, left=0.1, right=0.2, bottom=0.4) 113 | 114 | axbox = plt.axes([0.4, 0.85, 0.2, 0.075]) 115 | text_box0 = TextBox(axbox, 'Segment (seconds)', str(parameters['seg_size_sec'])) 116 | axbox = plt.axes([0.4, 0.75, 0.2, 0.075]) 117 | text_box1 = TextBox(axbox, 'Segment shift (seconds)', str(parameters['seg_shft_sec'])) 118 | axbox = plt.axes([0.4, 0.65, 0.2, 0.075]) 119 | text_box2 = TextBox(axbox, 'N Cycles', str(parameters['n_cycles'])) 120 | axbox = plt.axes([0.4, 0.55, 0.2, 0.075]) 121 | text_box3 = TextBox(axbox, 'Freq Conv. min Max (Hz)', str(parameters['freq_range']).strip('[').strip(']') ) 122 | axbox = plt.axes([0.4, 0.45, 0.2, 0.075]) 123 | text_box4 = TextBox(axbox, 'Spectr Pwr min Max (dB)', str(parameters['freq_color']).strip('[').strip(']')) 124 | axbox = plt.axes([0.4, 0.35, 0.2, 0.075]) 125 | text_box5 = TextBox(axbox, 'Freq Mod. min Max (Hz)', str(parameters['mfreq_range']).strip('[').strip(']')) 126 | axbox = plt.axes([0.4, 0.25, 0.2, 0.075]) 127 | text_box6 = TextBox(axbox, 'ModSpec Pwr min Max (dB)', str(parameters['mfreq_color']).strip('[').strip(']')) 128 | 129 | axbox = plt.axes([0.4, 0.05, 0.2, 0.075]) 130 | ok_button = Button(axbox, 'OK') 131 | 132 | boxes = [text_box0, text_box1, text_box2, text_box3, 133 | text_box4, text_box5, text_box6, ok_button] 134 | 135 | ok_button.on_clicked(submit) 136 | return 137 | 138 | def submit(text): 139 | global fig2 140 | global fig 141 | global cid 142 | plt.close(fig2) 143 | update_parameters() 144 | cid = fig.canvas.mpl_connect('key_press_event', press) 145 | 146 | def update_parameters(): 147 | global boxes 148 | global parameters 149 | # Pop reference to Button 150 | boxes.pop(-1) 151 | parameters['seg_size_sec'] = float(boxes[0].text) 152 | parameters['seg_shft_sec'] = float(boxes[1].text) 153 | parameters['n_cycles'] = float(boxes[2].text) 154 | if boxes[3].text == 'None': 155 | parameters['freq_range'] = None 156 | else: 157 | parameters['freq_range'] = np.fromstring(boxes[3].text, sep=' ') 158 | if boxes[4].text == 'None': 159 | parameters['freq_color'] = None 160 | else: 161 | parameters['freq_color'] = np.fromstring(boxes[4].text, sep=' ') 162 | if boxes[5].text == 'None': 163 | parameters['mfreq_range'] = None 164 | else: 165 | parameters['mfreq_range'] = np.fromstring(boxes[5].text, sep=' ') 166 | if boxes[6].text == 'None': 167 | parameters['mfreq_color'] = None 168 | else: 169 | parameters['mfreq_color'] = np.fromstring(boxes[6].text, sep=' ') 170 | 171 | first_run() 172 | 173 | return 174 | 175 | 176 | def update_plots(): 177 | global ix_segment 178 | global x_segments 179 | global n_cycles 180 | global ix_channel 181 | global x_probe 182 | global x_spectrogram 183 | global fig 184 | global channel_names 185 | global fs 186 | global parameters 187 | global gc_map 188 | 189 | fig.clear() 190 | 191 | # constrain ix_segment to [1 : n_segments] 192 | ix_segment = max([0, ix_segment]) 193 | ix_segment = min([n_segments, ix_segment]) 194 | 195 | # select segment 196 | x = x_segments[:, :, ix_segment] 197 | 198 | # compute and plot Modulation Spectrogram 199 | print('Computing modulation spectrogram...') 200 | x_wavelet_modspec = ama.wavelet_modulation_spectrogram(x, fs, n_cycles=n_cycles, fft_factor_x=2, channel_names=[channel_names[ix_channel]]) 201 | plt.subplot(4,2,(6,8)) 202 | ama.plot_modulation_spectrogram_data(x_wavelet_modspec, f_range=parameters['freq_range'], modf_range=parameters['mfreq_range'], c_range=parameters['mfreq_color'], c_map=gc_map) 203 | 204 | # plot time series for segment 205 | plt.subplot(4,2,5) 206 | ama.plot_signal(x, fs, channel_names[ix_channel]) 207 | plt.colorbar() 208 | time_lim = plt.xlim() 209 | 210 | # plot spectrogram for segment 211 | plt.subplot(4,2,7) 212 | ama.plot_spectrogram_data(x_wavelet_modspec['spectrogram_data'], f_range=parameters['freq_range'], c_range=parameters['freq_color'], c_map=gc_map ) 213 | plt.xlim(time_lim) 214 | 215 | # plot full signal 216 | h_ts = plt.subplot(4,2,(1,2)) 217 | ama.plot_signal(x_probe, fs, channel_names[ix_channel]) 218 | plt.colorbar() 219 | time_lim = plt.xlim() 220 | 221 | # plot spectrogram for full signal 222 | h_tf = plt.subplot(4,2,(3,4)) 223 | ama.plot_spectrogram_data(x_spectrogram, f_range=parameters['freq_range'], c_range=parameters['freq_color'], c_map=gc_map ) 224 | plt.xlim(time_lim) 225 | 226 | # highlight area under analysis in time series 227 | seg_ini_sec = (ix_segment ) * parameters['seg_shft_sec'] 228 | seg_end_sec = seg_ini_sec + parameters['seg_size_sec'] 229 | 230 | plt.subplot(h_ts) 231 | varea([seg_ini_sec, seg_end_sec ],'r',0.4) 232 | 233 | # highlight area under analysis in Spectrogram 234 | plt.subplot(h_tf) 235 | varea([seg_ini_sec, seg_end_sec ],'r',0.4) 236 | 237 | print('done!') 238 | 239 | # display information about analysis 240 | print('signal name : %s' % channel_names[ix_channel] ) 241 | print('segment size (seconds): %0.3f' % parameters['seg_size_sec']) 242 | print('segment shift (seconds): %0.3f' % parameters['seg_shft_sec']) 243 | print('segment position (sec): %0.3f' % seg_ini_sec) 244 | print('n cycles Complex Morlet: %0.3f' % parameters['n_cycles']) 245 | 246 | fig.canvas.draw() 247 | plt.show() 248 | return 249 | 250 | 251 | def varea(xlims, color_str, alpha_v=0.2): 252 | ax = plt.gca() 253 | ylims = ax.get_ylim() 254 | plt.fill((xlims[0], xlims[0], xlims[1], xlims[1]), 255 | (ylims[0], ylims[1], ylims[1], ylims[0]), 256 | color_str, alpha=alpha_v) 257 | return 258 | 259 | 260 | 261 | def explore_wavelet_ama_gui(x, fs_arg, channel_names_arg = None, c_map = 'viridis'): 262 | 263 | # Global variables 264 | global ix_channel 265 | global ix_segment 266 | global n_channels 267 | global n_segments 268 | global x_segments 269 | global cid 270 | global fig 271 | 272 | global parameters 273 | global gc_map 274 | 275 | global n_cycles 276 | 277 | global X 278 | global name 279 | global fs 280 | global channel_names 281 | 282 | fs = fs_arg 283 | channel_names = channel_names_arg 284 | X = x 285 | gc_map = c_map 286 | 287 | # input 'x' as 2D matrix [samples, columns] 288 | try: 289 | X.shape[1] 290 | except IndexError: 291 | X = X[:, np.newaxis] 292 | 293 | # number of channels 294 | n_channels = X.shape[1] 295 | 296 | if type(channel_names) == str and n_channels == 1: 297 | channel_names = [channel_names] 298 | # generate default channel names, if needed 299 | if channel_names is None or len(channel_names) != n_channels: 300 | channel_names = [] 301 | for ic in range (0 , n_channels): 302 | icp = ic + 1 303 | channel_names.append( str('Signal-%02d' % icp) ) 304 | 305 | 306 | 307 | #% Amplitude Modulation Analysis 308 | # Default Modulation Analysis parameters 309 | parameters = {} 310 | parameters['seg_size_sec'] = 8.0 # segment of signal to compute the Modulation Spectrogram (seconds) 311 | parameters['seg_shft_sec'] = 8.0 # shift between consecutive segments (seconds) 312 | parameters['n_cycles'] = 6 # number of cycles (for Complex Morlet) 313 | parameters['freq_range'] = None # limits [min, max] for the conventional frequency axis (Hz) 314 | parameters['mfreq_range'] = None # limits [min, max] for the modulation frequency axis (Hz) 315 | parameters['freq_color'] = None # limits [min, max] for the power in Spectrogram (dB) 316 | parameters['mfreq_color'] = None # limits [min, max] for the power in Modulation Spectrogram (dB) 317 | 318 | # initial channel and segment 319 | ix_channel = 0 320 | ix_segment = 0 321 | 322 | # other variables 323 | n_segments = None 324 | 325 | x_segments = None 326 | name = None 327 | n_cycles = None 328 | 329 | # Live GUI 330 | fig = plt.figure() 331 | first_run() 332 | cid = fig.canvas.mpl_connect('key_press_event', press) 333 | 334 | -------------------------------------------------------------------------------- /am_analysis/msqi_ama.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jun 17 18:45:19 2019 5 | 6 | @author: cassani 7 | """ 8 | 9 | from . import am_analysis as ama 10 | import numpy as np 11 | 12 | def msqi_ama(x, fs): 13 | """ 14 | Computes the Modulation Spectrum-Based ECG Quality Index (MSQI) for one or 15 | many ECG signals defined in x, sampled with a sampling frequency fs 16 | 17 | Parameters 18 | ---------- 19 | x : 1D array with shape (n_samples) or 20 | 2D array with shape (n_samples, n_signals) 21 | fs : Sampling frequency in Hz 22 | 23 | Returns 24 | ------- 25 | msqi_value : MSQI value or values 26 | hr_value : HR values or values 27 | modulation_spectrogram : Structure or structures of modulation spectrogram 28 | 29 | See 30 | -------- 31 | MS-QI: A Modulation Spectrum-Based ECG Quality Index for Telehealth Applications 32 | http://ieeexplore.ieee.org/document/6892964/ 33 | 34 | D. P. Tobon V., T. H. Falk, and M. Maier, "MS-QI: A Modulation 35 | Spectrum-Based ECG Quality Index for Telehealth Applications", IEEE 36 | Transactions on Biomedical Engineering, vol. 63, no. 8, pp. 1613-1622, 37 | Aug. 2016 38 | """ 39 | 40 | # test ecg shape 41 | try: 42 | x.shape[1] 43 | except IndexError: 44 | x = x[:, np.newaxis] 45 | 46 | # Empirical values for the STFFT transformation 47 | win_size_sec = 0.125 #seconds 48 | win_over_sec = 0.09375 #seconds 49 | nfft_factor_1 = 16 50 | nfft_factor_2 = 4 51 | 52 | win_size_smp = int(win_size_sec * fs) #samples 53 | win_over_smp = int(win_over_sec * fs) #samples 54 | win_shft_smp = win_size_smp - win_over_smp 55 | 56 | # Computes Modulation Spectrogram 57 | modulation_spectrogram = ama.strfft_modulation_spectrogram(x, fs, win_size_smp, 58 | win_shft_smp, nfft_factor_1, 'cosine', nfft_factor_2, 'cosine' ) 59 | 60 | # Find fundamental frequency (HR) 61 | # f = (0, 40)Hz 62 | ix_f_00 = (np.abs(modulation_spectrogram['freq_axis'] - 0)).argmin(0) 63 | ix_f_40 = (np.abs(modulation_spectrogram['freq_axis'] - 40)).argmin(0) + 1 64 | 65 | # Look for the maximum only from 0.6 to 3 Hz (36 to 180 bpm) 66 | valid_f_ix = np.logical_or(modulation_spectrogram['freq_mod_axis'] < 0.66 , modulation_spectrogram['freq_mod_axis'] > 3) 67 | 68 | # number of epochs 69 | n_epochs = modulation_spectrogram['power_modulation_spectrogram'].shape[2] 70 | 71 | msqi_vals = np.zeros(n_epochs) 72 | hr_vals = np.zeros(n_epochs) 73 | 74 | for ix_epoch in range(n_epochs): 75 | B = np.sqrt(modulation_spectrogram['power_modulation_spectrogram'][:, :, ix_epoch]) 76 | 77 | # Scale to maximun of B 78 | B = B / np.max(B) 79 | 80 | # Add B in the conventional frequency axis from 0 to 40 Hz 81 | tmp = np.sum(B[ix_f_00:ix_f_40, :], axis=0) 82 | 83 | # Look for the maximum only from 0.6 to 3 Hz (36 to 180 bpm) 84 | tmp[valid_f_ix] = 0 85 | ix_max = np.argmax(tmp) 86 | freq_funda = modulation_spectrogram['freq_mod_axis'][ix_max] 87 | 88 | # TME 89 | tme = np.sum(B) 90 | 91 | eme = 0 92 | for ix_harm in range(1, 5): 93 | ix_fm = (np.abs(modulation_spectrogram['freq_mod_axis'] - (ix_harm * freq_funda) )).argmin(0) 94 | ix_b = int(round(.3125 / modulation_spectrogram['freq_mod_delta'] )) # 0.3125Hz, half lobe 95 | # EME 96 | eme = eme + np.sum(B[ 0 : ix_f_40, ix_fm - ix_b : ix_fm + ix_b + 1 ]) 97 | 98 | # RME 99 | rme = tme - eme 100 | # MS-QI 101 | msqi_vals[ix_epoch] = eme / rme 102 | # HR 103 | hr_vals[ix_epoch] = freq_funda * 60 104 | 105 | return (msqi_vals, hr_vals, modulation_spectrogram) -------------------------------------------------------------------------------- /example_01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example 01 5 | This example shows the use of the GUI to explore Amplitude Modulations 6 | for ECG data and EEG data 7 | 8 | The 'explore_strfft_am_gui()' computes the Modulation Spectrogram. 9 | It uses the Short Time Real Fourier Fast Transform (STRFFT) to compute 10 | the Spectrogram, after rFFT is used to obtain the Modulation Spectrogram 11 | 12 | The 'explore_wavelet_am_gui()' computes the Modulation Spectrogram using 13 | It uses the Wavelet transform with Complex Morlet wavelet to compute 14 | the Spectrogram, after rFFT is used to obtain the Modulation Spectrogram 15 | 16 | Usage for explore_*_am_gui() 17 | 18 | Once the GUI is executed, it accepts the following commands 19 | 20 | Key Action 21 | Up Arrow Previous channel (-1 channel) 22 | Down Arrow Next channel (+1 channel) 23 | Left Arrow Go back to the previous segment (-1 segment shift) 24 | Right Arrow Advance to the next segment (+1 segment shift) 25 | 'W' Previous channel (-5 channels) 26 | 'S' Next channel (+5 channel) 27 | 'A' Go back to the previous segment (-5 segment shift) 28 | 'D' Advance to the next segment (+5 segment shift) 29 | 30 | 'U' Menu to update: 31 | parameters for Modulation Spectrogram 32 | ranges for conventional and modulation frequency axes 33 | ranges for power in Spectrogram and Modulation Spectrogram 34 | ESC Close the GUI 35 | """ 36 | 37 | import pickle 38 | from am_analysis.explore_stfft_ama_gui import explore_stfft_ama_gui 39 | from am_analysis.explore_wavelet_ama_gui import explore_wavelet_ama_gui 40 | 41 | 42 | if __name__ == "__main__": 43 | 44 | #% ECG data (1 channel) using STFFT-based Modulation Spectrogram 45 | [x, fs] = pickle.load(open( "./example_data/ecg_data.pkl", "rb" )) 46 | # STFFT Modulation Spectrogram 47 | explore_stfft_ama_gui(x, fs, ['ECG']) 48 | 49 | #% ECG data (1 channel) using wavelet-based Modulation Spectrogram 50 | [x, fs] = pickle.load(open( "./example_data/ecg_data.pkl", "rb" )) 51 | # Wavelet Modulation Spectrogram 52 | explore_wavelet_ama_gui(x, fs, ['ECG']) 53 | 54 | #% EEG data (7 channels) using STFFT-based Modulation Spectrogram 55 | [x, fs, ch_names] = pickle.load(open( "./example_data/eeg_data.pkl", "rb" )) 56 | # STFFT Modulation Spectrogram 57 | explore_stfft_ama_gui(x, fs, ch_names) 58 | 59 | #% EEG data (7 channels) using wavelet-based Modulation Spectrogram 60 | [x, fs, ch_names] = pickle.load(open( "./example_data/eeg_data.pkl", "rb" )) 61 | # Wavelet Modulation Spectrogram 62 | explore_wavelet_ama_gui(x, fs, ch_names) 63 | -------------------------------------------------------------------------------- /example_02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example 02 5 | Script to show the use of the functions: 6 | 7 | rfft_psd() Compute PSD using rFFT 8 | strfft_spectrogram() Compute Spectrogram using STFFT 9 | strfft_modulation_spectrogram() Compute Modulation Spectrogram using STFFT 10 | wavelet_spectrogram() Compute Spectrogram using wavelet transformation 11 | wavelet_modulation_spectrogram() Compute Modulation Spectrogram using wavelet transformation 12 | 13 | plot_signal() Plot a signal in time domain 14 | plot_psd_data() Plot PSD data obtained with rfft_psd() 15 | plot_spectrogram_data() Plot Spectrogram data obtained with 16 | strfft_spectrogram() or wavelet_spectrogram() 17 | plot_modulation_spectrogram_data() Plot Modulation Spectrogram data obtained with 18 | strfft_modspectrogram() or wavelet_modspectrogram() 19 | 20 | Moreover, this script compares diverse ways to compute the power of a 21 | signal in the Time, Frequency, Time-Frequency and Frequency-Frequency domains 22 | 23 | """ 24 | import numpy as np 25 | import matplotlib.pyplot as plt 26 | from am_analysis import am_analysis as ama 27 | 28 | if __name__ == "__main__": 29 | 30 | #Test signal 31 | 32 | fs = 500 33 | T = 1/fs 34 | t1_vct = np.arange(10*fs)/fs 35 | 36 | x1 = 3 * np.sin (2 * np.pi * 10 * t1_vct) 37 | x2 = 2 * np.sin (2 * np.pi * 24 * t1_vct) 38 | x3 = 1 * np.random.randn(t1_vct.shape[0]) 39 | 40 | x = np.concatenate([x1, x2, x3]) 41 | 42 | n = x.shape[0] 43 | 44 | # Plot signal 45 | plt.figure() 46 | ama.plot_signal(x, fs, 'test-signal') 47 | 48 | # Power in Time Domain 49 | # Energy of the signal 50 | energy_x = T * sum(x**2) 51 | duration = T * n 52 | 53 | # Power of the signal 54 | power_x = energy_x / duration 55 | 56 | # A simpler way is 57 | power_x_2 = (1 / n) * sum(x**2) 58 | 59 | # Power in Frequency domain 60 | # Power using FFT 61 | X = np.fft.fft(x) 62 | power_X = float( (1 / n**2) * sum(np.abs(X)**2)) 63 | 64 | # Power using its PSD from rFFT 65 | psd_rfft_r = ama.rfft_psd(x, fs, win_function = 'boxcar') 66 | f_step = psd_rfft_r['freq_delta'] 67 | power_psd_rfft_x_rw = f_step * sum(psd_rfft_r['PSD'])[0] 68 | plt.figure() 69 | ama.plot_psd_data(psd_rfft_r) 70 | 71 | # Power using its PSD from rFFT 72 | psd_rfft_b = ama.rfft_psd(x, fs, win_function = 'blackmanharris') 73 | f_step = psd_rfft_r['freq_delta'] 74 | power_psd_rfft_x_bh = f_step * sum(psd_rfft_r['PSD'])[0] 75 | plt.figure() 76 | ama.plot_psd_data(psd_rfft_b) 77 | 78 | # Power from STFFT Spectrogram (Hamming window) 79 | w_size = 1 * fs 80 | w_shift = 0.5 * w_size 81 | rfft_spect_h = ama.strfft_spectrogram(x, fs, w_size, w_shift, win_function = 'hamming' ) 82 | power_spect_h = sum(sum(rfft_spect_h['power_spectrogram']))[0] * rfft_spect_h['freq_delta'] * rfft_spect_h['time_delta'] 83 | plt.figure() 84 | ama.plot_spectrogram_data(rfft_spect_h) 85 | 86 | # Power from STFFT Spectrogram (Rectangular window) 87 | w_size = 1 * fs 88 | w_shift = 0.5 * w_size 89 | rfft_spect_r = ama.strfft_spectrogram(x, fs, w_size, w_shift, win_function = 'boxcar') 90 | power_spect_r = sum(sum(rfft_spect_r['power_spectrogram']))[0] * rfft_spect_r['freq_delta'] * rfft_spect_r['time_delta'] 91 | plt.figure() 92 | ama.plot_spectrogram_data(rfft_spect_r) 93 | 94 | # Power from Wavelet Spectrogram N = 6 95 | wav_spect_6 = ama.wavelet_spectrogram(x, fs, 6) 96 | power_wav_6 = sum(sum(wav_spect_6['power_spectrogram']))[0] * wav_spect_6['freq_delta'] * wav_spect_6['time_delta'] 97 | plt.figure() 98 | ama.plot_spectrogram_data(wav_spect_6) 99 | 100 | # Power from Wavelet Spectrogram N = 10 101 | wav_spect_10 = ama.wavelet_spectrogram(x, fs, 10) 102 | power_wav_10 = sum(sum(wav_spect_10['power_spectrogram']))[0] * wav_spect_10['freq_delta'] * wav_spect_10['time_delta'] 103 | plt.figure() 104 | ama.plot_spectrogram_data(wav_spect_10) 105 | 106 | # Power from Modulation Spectrogram STFFT 107 | w_size = 1 * fs 108 | w_shift = 0.5 * w_size 109 | rfft_mod_b = ama.strfft_modulation_spectrogram(x, fs, w_size, w_shift, win_function_y = 'boxcar', win_function_x = 'boxcar') 110 | power_mod = sum(sum(rfft_mod_b['power_modulation_spectrogram']))[0] * rfft_mod_b['freq_delta'] * rfft_mod_b['freq_mod_delta'] 111 | plt.figure() 112 | ama.plot_modulation_spectrogram_data(rfft_mod_b) 113 | 114 | # Power from Modulation Spectrogram Wavelet 115 | wav_mod_6 = ama.wavelet_modulation_spectrogram(x, fs, 6, win_function_x = 'boxcar') 116 | power_mod_w = sum(sum(wav_mod_6['power_modulation_spectrogram']))[0] * wav_mod_6['freq_delta'] * wav_mod_6['freq_mod_delta'] 117 | plt.figure() 118 | ama.plot_modulation_spectrogram_data(wav_mod_6) 119 | 120 | plt.show() 121 | 122 | 123 | -------------------------------------------------------------------------------- /example_03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example 03 5 | Script to show the computation of several Modulation Spectrograms from a signal 6 | 7 | The Modulation Spectrogram are computed with the Wavelet transform 8 | 9 | Method A. 10 | The signal, is segmented an a Modulation Spectrogram computed per Segment 11 | 12 | Method B. 13 | The Spectrogram of the Full signal is obtained, after this Spectrogram 14 | is segmented, and from each Segment the Modulation Spectrogram is 15 | derived 16 | 17 | 18 | Moreover, this script compares diverse ways to compute the power of a 19 | signal in the Time, Time-Frequency and Frequency-Frequency domains 20 | """ 21 | import pickle 22 | import numpy as np 23 | import matplotlib.pyplot as plt 24 | from am_analysis import am_analysis as ama 25 | import time 26 | from copy import deepcopy 27 | 28 | if __name__ == "__main__": 29 | 30 | # ECG data (1 channel) 31 | [x, fs] = pickle.load(open( "./example_data/ecg_data.pkl", "rb" )) 32 | 33 | # Segment parameters 34 | segment_length = 5 # seconds 35 | segment_overlap = 0 # seconds 36 | 37 | # Power in Time Domain 38 | T = 1 / fs; 39 | n = x.shape[0] 40 | 41 | # Power in Time Domain 42 | # Energy of the signal 43 | energy_x = T * sum(x**2)[0] 44 | duration = T * n 45 | 46 | # Power of the signal 47 | power_x = energy_x / duration 48 | 49 | # A. Epoching > Spectrogram > Modulation 50 | tic = time.time() 51 | 52 | # Epochiong data 53 | x_segmented, _, _ = ama.epoching(x, round(segment_length * fs) ) 54 | n_segments = x_segmented.shape[2] 55 | 56 | wavelet_spectrogram_data_a = [] 57 | wavelet_modulation_spectrogram_data_a = [] 58 | 59 | # For each Segment, compute its Spectrogram and Modulation Spectrogram 60 | for i_segment in range(0, n_segments): 61 | x_tmp_wavelet = x_segmented[:,:,i_segment] 62 | 63 | # Wavelet-based Spectrogram and Modulation Spectrogram 64 | wavelet_spectrogram_data_a.append(ama.wavelet_spectrogram(x_tmp_wavelet, fs)) 65 | wavelet_modulation_spectrogram_data_a.append(ama.wavelet_modulation_spectrogram(x_tmp_wavelet, fs)) 66 | 67 | toc = time.time() - tic 68 | print(str(toc) + ' seconds') 69 | 70 | # B. Spectrogram > Epoching > Modulation 71 | tic = time.time() 72 | 73 | # Spectrogram of the Full Signal with STFFT and Wavelets 74 | wavelet_spect_data = ama.wavelet_spectrogram(x ,fs) 75 | 76 | # Epoching the Spectrogram 77 | wavelet_spect_segmented, _, _ = ama.epoching(np.squeeze(wavelet_spect_data['power_spectrogram']), round(segment_length * fs)) 78 | n_segments = wavelet_spect_segmented.shape[2] 79 | 80 | # The Spectograms are scaled to represent the power of the full signal 81 | wavelet_spect_segmented = n_segments * wavelet_spect_segmented 82 | 83 | wavelet_spectrogram_power_b = [] 84 | wavelet_modulation_spectrogram_power_b = [] 85 | 86 | # From each Segment of the Spectrogram, compute the Modulation Spectrogram 87 | for i_segment in range(0, n_segments): 88 | wavelet_spectrogram_power_b.append(wavelet_spect_segmented[:,:,i_segment]) 89 | 90 | # Square Root is obtained to work with the Instantaneous Amplitude 91 | x_tmp_wavelet = np.sqrt(np.squeeze(wavelet_spect_segmented[:,:, i_segment])) 92 | 93 | # PSD of the Spectrogram Segment 94 | mod_psd_wavelet = ama.rfft_psd(x_tmp_wavelet, fs) 95 | 96 | # Place results in corresponding index 97 | wavelet_modulation_spectrogram_power_b.append(mod_psd_wavelet['PSD'] / mod_psd_wavelet['freq_delta']) 98 | 99 | toc = time.time() - tic 100 | print(str(toc) + ' seconds') 101 | 102 | # Create dictionaries for Method B, for sake of plotting 103 | wavelet_spectrogram_data_b = deepcopy(wavelet_spectrogram_data_a) 104 | wavelet_modulation_spectrogram_data_b = deepcopy(wavelet_modulation_spectrogram_data_a) 105 | 106 | for i_segment in range(0, n_segments): 107 | wavelet_spectrogram_data_b[i_segment]['power_spectrogram'] = wavelet_spectrogram_power_b[i_segment][:,:,np.newaxis] 108 | wavelet_modulation_spectrogram_data_b[i_segment]['power_modulation_spectrogram'] = np.transpose(wavelet_modulation_spectrogram_power_b[i_segment])[:,:,np.newaxis] 109 | 110 | 111 | # Comparison 112 | # One segment is randomly chosen 113 | random_segment = np.random.randint(0, n_segments) 114 | 115 | pwr_spectrogram_wavelet_a = np.zeros(n_segments) 116 | pwr_spectrogram_wavelet_b = np.zeros(n_segments) 117 | pwr_modulation_spectrogram_wavelet_a = np.zeros(n_segments) 118 | pwr_modulation_spectrogram_wavelet_b = np.zeros(n_segments) 119 | 120 | 121 | for i_segment in range(0, n_segments): 122 | if i_segment == random_segment: 123 | plt.figure() 124 | plt.subplot(1,2,1) 125 | ama.plot_spectrogram_data(wavelet_spectrogram_data_a[i_segment], 0); 126 | plt.subplot(1,2,2) 127 | ama.plot_spectrogram_data(wavelet_spectrogram_data_b[i_segment], 0); 128 | 129 | plt.figure() 130 | plt.subplot(1,2,1) 131 | ama.plot_modulation_spectrogram_data(wavelet_modulation_spectrogram_data_a[i_segment], 0); 132 | plt.subplot(1,2,2) 133 | ama.plot_modulation_spectrogram_data(wavelet_modulation_spectrogram_data_b[i_segment], 0); 134 | 135 | pwr_spectrogram_wavelet_a[i_segment] = sum(sum(wavelet_spectrogram_data_a[i_segment]['power_spectrogram'])) * wavelet_spectrogram_data_a[0]['freq_delta'] * wavelet_spectrogram_data_a[0]['time_delta'] 136 | pwr_spectrogram_wavelet_b[i_segment] = sum(sum(wavelet_spectrogram_data_b[i_segment]['power_spectrogram'])) * wavelet_spectrogram_data_b[0]['freq_delta'] * wavelet_spectrogram_data_b[0]['time_delta'] 137 | 138 | pwr_modulation_spectrogram_wavelet_a[i_segment] = sum(sum(wavelet_modulation_spectrogram_data_a[i_segment]['power_modulation_spectrogram'])) * wavelet_modulation_spectrogram_data_a[0]['freq_delta'] * wavelet_modulation_spectrogram_data_a[0]['freq_mod_delta'] 139 | pwr_modulation_spectrogram_wavelet_b[i_segment] = sum(sum(wavelet_modulation_spectrogram_data_b[i_segment]['power_modulation_spectrogram'])) * wavelet_modulation_spectrogram_data_b[0]['freq_delta'] * wavelet_modulation_spectrogram_data_b[0]['freq_mod_delta'] 140 | 141 | # Power comparison Spectrogram and Modulation Spectrogram 142 | plt.figure() 143 | plt.title('Total Power per Epoch, based on Spectrogram') 144 | plt.plot(pwr_spectrogram_wavelet_a, label = 'Wavelet Spectrogram A') 145 | plt.plot(pwr_spectrogram_wavelet_b, label = 'Wavelet Spectrogram B') 146 | plt.legend() 147 | print('Mean Power Spectrogram A: ' + str(np.mean(pwr_spectrogram_wavelet_a)) ) 148 | print('Mean Power Spectrogram B: ' + str(np.mean(pwr_spectrogram_wavelet_b)) ) 149 | 150 | 151 | plt.figure() 152 | plt.title('Total Power per Epoch, based on Modulation Spectrogram') 153 | plt.plot(pwr_modulation_spectrogram_wavelet_a, label = 'Wavelet Modulation Spectrogram A') 154 | plt.plot(pwr_modulation_spectrogram_wavelet_b, label = 'Wavelet Modulation Spectrogram B') 155 | plt.legend() 156 | print('Mean Power Modulation Spectrogram A: ' + str(np.mean(pwr_modulation_spectrogram_wavelet_a)) ) 157 | print('Mean Power Modulation Spectrogram B: ' + str(np.mean(pwr_modulation_spectrogram_wavelet_b)) ) 158 | 159 | plt.show() 160 | -------------------------------------------------------------------------------- /example_04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example 04 5 | This example shows the amplitude modulation analysis toolbox for speech data 6 | 7 | rfft_psd() Compute PSD using rFFT 8 | strfft_spectrogram() Compute Spectrogram using STFFT 9 | strfft_modulation_spectrogram() Compute Modulation Spectrogram using STFFT 10 | wavelet_spectrogram() Compute Spectrogram using wavelet transformation 11 | wavelet_modulation_spectrogram() Compute Modulation Spectrogram using wavelet transformation 12 | 13 | plot_signal() Plot a signal in time domain 14 | plot_psd_data() Plot PSD data obtained with rfft_psd() 15 | plot_spectrogram_data() Plot Spectrogram data obtained with 16 | strfft_spectrogram() or wavelet_spectrogram() 17 | plot_modulation_spectrogram_data() Plot Modulation Spectrogram data obtained with 18 | strfft_modspectrogram() or wavelet_modspectrogram() 19 | 20 | Moreover, this script compares diverse ways to compute the power of a 21 | signal in the Time, Frequency, Time-Frequency and Frequency-Frequency domains 22 | 23 | """ 24 | import numpy as np 25 | import matplotlib.pyplot as plt 26 | from scipy.io import wavfile 27 | from am_analysis import am_analysis as ama 28 | 29 | if __name__ == "__main__": 30 | 31 | # speech signal 32 | # The speech signal p234_004.wav is one sample from the: 33 | # CSTR VCTK Corpus: English Multi-speaker Corpus for CSTR Voice Cloning Toolkit 34 | # avialable in: https://datashare.is.ed.ac.uk/handle/10283/2651 35 | fs, x = wavfile.read('./example_data/p234_004.wav') 36 | x_name = ['speech'] 37 | x = x / 32768 38 | # 1s segment to analyze 39 | x = x[int(fs*1.6) : int(fs*3.6)] 40 | 41 | 42 | #%% STFT-based 43 | # Parameters 44 | win_size_sec = 0.04 # window length for the STFFT (seconds) 45 | win_shft_sec = 0.01 # shift between consecutive windows (seconds) 46 | 47 | plt.figure() 48 | 49 | stft_spectrogram = ama.strfft_spectrogram(x, fs, win_size = round(win_size_sec*fs), win_shift = round(win_shft_sec*fs), channel_names = x_name) 50 | plt.subplot2grid((4,5),(1,0),rowspan=1, colspan=5) 51 | ama.plot_spectrogram_data(stft_spectrogram) 52 | 53 | plt.subplot2grid((4,5),(0,0),rowspan=1, colspan=5) 54 | ama.plot_signal(x, fs, x_name[0]) 55 | plt.colorbar() 56 | 57 | stft_modulation_spectrogram = ama.strfft_modulation_spectrogram(x, fs, win_size = round(win_size_sec*fs), win_shift = round(win_shft_sec*fs), channel_names = x_name) 58 | plt.subplot2grid((4,5), (2,1),rowspan=2, colspan=3) 59 | ama.plot_modulation_spectrogram_data(stft_modulation_spectrogram, 0 , modf_range = np.array([0,20]), c_range = np.array([-90, -50])) 60 | 61 | 62 | #%% Parameters for CWT for speech signal 63 | n_cycles = 6 # number of cycles (for Complex Morlet) 64 | up_lim = np.floor(np.log(fs/2) / np.log(2)) 65 | frequency_vector = 2**np.arange(1,up_lim,0.2) # vector of frequencies to compute the CWT 66 | 67 | plt.figure() 68 | 69 | cwt_spectrogram = ama.wavelet_spectrogram(x, fs, n_cycles, frequency_vector, channel_names=x_name) 70 | plt.subplot2grid((4,5),(1,0),rowspan=1, colspan=5) 71 | ama.plot_spectrogram_data(cwt_spectrogram) 72 | 73 | plt.subplot2grid((4,5),(0,0),rowspan=1, colspan=5) 74 | ama.plot_signal(x, fs, x_name[0]) 75 | plt.colorbar() 76 | 77 | cwt_modulation_spectrogram = ama.wavelet_modulation_spectrogram(x, fs, n_cycles, frequency_vector, channel_names = x_name) 78 | plt.subplot2grid((4,5), (2,1),rowspan=2, colspan=3) 79 | ama.plot_modulation_spectrogram_data(cwt_modulation_spectrogram, 0, modf_range=np.array([0,20]), c_range=np.array([-90, -50])) 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /example_05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example 05 5 | This example shows the use of the transfomrs and their inverses 6 | 7 | rfft() Fourier transform for real-valued signals 8 | irfft() Inverse Fourier transform for real-valued signals 9 | 10 | rfft_psd() Computes PSD data from x(f) 11 | irfft_psd() Recovers x(t) from its PSD data 12 | 13 | strfft_spectrogram() Computes Spectrogram data using STFFT 14 | istrfft_modulation_spectrogram() Recovers x(t) from its STFFT Spectrogram data 15 | 16 | wavelet_spectrogram() Computes Spectrogram using CWT 17 | iwavelet_modulation_spectrogram() Recovers x(t) from its CWT Spectrogram data 18 | """ 19 | import numpy as np 20 | import matplotlib.pyplot as plt 21 | from am_analysis import am_analysis as ama 22 | 23 | if __name__ == "__main__": 24 | 25 | #Test signal 26 | fs = 2000 27 | time_v = np.arange(10*fs)/fs 28 | 29 | f1 = np.sin(2 * np.pi * 100 * time_v) 30 | f2 = np.sin(2 * np.pi * 325 * time_v) 31 | m1 = np.sin(2 * np.pi * 5 * time_v) + 2 32 | m2 = np.sin(2 * np.pi * 3 * time_v) + 2 33 | 34 | xi = f1*m1 + f2*m2 35 | xi = xi[:, np.newaxis] 36 | n = xi.shape[0] 37 | 38 | plt.figure() 39 | ama.plot_signal(xi, fs) 40 | 41 | #%% time <--> frequency 42 | #%% FFT and IFFT of a real-valued signal 43 | xi_rfft = ama.rfft(xi) 44 | xo = ama.irfft(xi_rfft, n) 45 | 46 | plt.figure() 47 | fi = plt.subplot(2,1,1) 48 | ama.plot_signal(xi, fs, 'Original x(t)') 49 | fo = plt.subplot(2,1,2, sharex=fi, sharey=fi) 50 | ama.plot_signal(xo, fs, 'Recovered x(t)') 51 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 52 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 53 | 54 | #%% PSD data obtained with rFFT, and its inverse 55 | xi_psd = ama.rfft_psd(xi, fs) 56 | xo = ama.irfft_psd(xi_psd) 57 | 58 | plt.figure() 59 | fi = plt.subplot(4,1,1) 60 | ama.plot_signal(xi, fs, 'Original x(t)') 61 | fo = plt.subplot(4,1,4, sharex=fi, sharey=fi) 62 | ama.plot_signal(xo, fs, 'Recovered x(t)') 63 | plt.subplot(4,1,(2,3)) 64 | ama.plot_psd_data(xi_psd) 65 | plt.title('PSD of x(t)') 66 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 67 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 68 | 69 | #%% time <--> time-frequency 70 | #%% STFT Spectrogram 71 | xi_strfft = ama.strfft_spectrogram(xi, fs, round(fs * 0.1), round(fs * 0.05)) 72 | xo = ama.istrfft_spectrogram(xi_strfft)[0] 73 | 74 | plt.figure() 75 | fi = plt.subplot(4,1,1) 76 | ama.plot_signal(xi, fs, 'Original x(t)') 77 | fo = plt.subplot(4,1,4, sharex=fi, sharey=fi) 78 | ama.plot_signal(xo, fs, 'Recovered x(t)') 79 | plt.subplot(4,1,(2,3)) 80 | ama.plot_spectrogram_data(xi_strfft) 81 | plt.title('STFT Spectrogram of x(t)') 82 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 83 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 84 | 85 | #%% CWT Spectrogram 86 | xi_cwt = ama.wavelet_spectrogram(xi, fs) 87 | xo = ama.iwavelet_spectrogram(xi_cwt) 88 | 89 | plt.figure() 90 | fi = plt.subplot(4,1,1) 91 | ama.plot_signal(xi, fs, 'Original x(t)') 92 | fo = plt.subplot(4,1,4, sharex=fi, sharey=fi) 93 | ama.plot_signal(xo, fs, 'Recovered x(t)') 94 | plt.subplot(4,1,(2,3)) 95 | ama.plot_spectrogram_data(xi_cwt) 96 | plt.title('CWT Spectrogram of x(t)') 97 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 98 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 99 | 100 | #%% time <--> frequency-modulation-frequency 101 | #%% STFT Modulation Spectrogram 102 | xi_mod_strfft = ama.strfft_modulation_spectrogram(xi, fs, round(fs * 0.1), round(fs * 0.05)) 103 | xo = ama.istrfft_modulation_spectrogram(xi_mod_strfft) 104 | 105 | plt.figure() 106 | fi = plt.subplot(4,1,1) 107 | ama.plot_signal(xi, fs, 'Original x(t)') 108 | fo = plt.subplot(4,1,4, sharex=fi, sharey=fi) 109 | ama.plot_signal(xo, fs, 'Recovered x(t)') 110 | plt.subplot(4,1,(2,3)) 111 | ama.plot_modulation_spectrogram_data(xi_mod_strfft, f_range=np.array([0, 1000]), modf_range=np.array([0, 10])) 112 | plt.title('STFT Modulation Spectrogram of x(t)') 113 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 114 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 115 | 116 | #%% CWT Modulation Spectrogram 117 | xi_mod_cwt = ama.wavelet_modulation_spectrogram(xi, fs) 118 | xo = ama.iwavelet_modulation_spectrogram(xi_mod_cwt) 119 | 120 | plt.figure() 121 | fi = plt.subplot(4,1,1) 122 | ama.plot_signal(xi, fs, 'Original x(t)') 123 | fo = plt.subplot(4,1,4, sharex=fi, sharey=fi) 124 | ama.plot_signal(xo, fs, 'Recovered x(t)') 125 | plt.subplot(4,1,(2,3)) 126 | ama.plot_modulation_spectrogram_data(xi_mod_cwt, f_range=np.array([0, 1000]), modf_range=np.array([0, 10])) 127 | plt.title('CWT Modulation Spectrogram of x(t)') 128 | r = np.corrcoef(np.squeeze(xi), np.squeeze(xo)) 129 | print('Correlation: ' + str(r[0,1]) + '\r\n' ) 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /example_data/ecg_data.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuSAELab/amplitude-modulation-analysis-module/34881a414123f628bc84320be2aed72ef0778be0/example_data/ecg_data.pkl -------------------------------------------------------------------------------- /example_data/eeg_data.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuSAELab/amplitude-modulation-analysis-module/34881a414123f628bc84320be2aed72ef0778be0/example_data/eeg_data.pkl -------------------------------------------------------------------------------- /example_data/info.txt: -------------------------------------------------------------------------------- 1 | The speech signal (p234_004.wav) consists in one sample from the: 2 | CSTR VCTK Corpus: English Multi-speaker Corpus for CSTR Voice Cloning Toolkit 3 | avialable in: https://datashare.is.ed.ac.uk/handle/10283/2651 4 | 5 | p234_004.wav says: "We also need a small plastic snake and a big toy frog for the kids". -------------------------------------------------------------------------------- /example_data/msqi_ecg_data.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuSAELab/amplitude-modulation-analysis-module/34881a414123f628bc84320be2aed72ef0778be0/example_data/msqi_ecg_data.pkl -------------------------------------------------------------------------------- /example_data/p234_004.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuSAELab/amplitude-modulation-analysis-module/34881a414123f628bc84320be2aed72ef0778be0/example_data/p234_004.wav -------------------------------------------------------------------------------- /example_msqi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | %% Example MSQI 5 | % This example shows the use of the amplitude modulation analysis toolkit 6 | % to compute the Modulation Spectrum-Based ECG Quality Index (MSQI) as a 7 | % blind metric to measure the signal-to-noise (SNR) ration in ECG signals 8 | % 9 | % The MSQI was originally presented in: 10 | % 11 | % D. P. Tobon V., T. H. Falk, and M. Maier, "MS-QI: A Modulation 12 | % Spectrum-Based ECG Quality Index for Telehealth Applications", IEEE 13 | % Transactions on Biomedical Engineering, vol. 63, no. 8, pp. 1613-1622, 14 | % Aug. 2016 15 | 16 | """ 17 | 18 | import pickle 19 | import numpy as np 20 | import matplotlib.pyplot as plt 21 | from am_analysis import am_analysis as ama 22 | from am_analysis.msqi_ama import msqi_ama 23 | 24 | 25 | #%% ECG signal 26 | # load ECG signal 27 | # x_clean is a 5-s segment of clean ECG signal 28 | # x_noisy is the x_clean signal contaminated with pink noise to have a 0db SNR 29 | [x_clean, x_noisy, fs] = pickle.load(open( "./example_data/msqi_ecg_data.pkl", "rb" )) 30 | 31 | #%% Compute MSQI and heart rate (HR), and plot modulation spectrogram 32 | [msqi_clean, hr_clean, modulation_spectrogram_clean] = msqi_ama(x_clean, fs) 33 | [msqi_noisy, hr_noisy, modulation_spectrogram_noisy] = msqi_ama(x_noisy, fs) 34 | 35 | print('HR = {:0.2f} bpm'.format(hr_clean[0])) 36 | print('MSQI for clean ECG = {:0.3f}'.format(msqi_clean[0])) 37 | print('MSQI for noisy ECG = {:0.3f}'.format(msqi_noisy[0])) 38 | 39 | #%% Plot modulation spectrograms 40 | plt.figure() 41 | mng = plt.get_current_fig_manager() 42 | mng.window.showMaximized() 43 | # clean 44 | plt.subplot(3,2,1) 45 | ama.plot_signal(x_clean, fs) 46 | plt.title('Clean ECG') 47 | plt.subplot(3,2,(3,5)) 48 | ama.plot_modulation_spectrogram_data(modulation_spectrogram_clean, f_range=np.array([0, 60]), c_range=np.array([-90, -40])) 49 | plt.title('Modulation spectrogram clean ECG') 50 | plt.subplot(3,2,2) 51 | ama.plot_signal(x_noisy, fs) 52 | plt.title('Noisy ECG') 53 | plt.subplot(3,2,(4,6)) 54 | ama.plot_modulation_spectrogram_data(modulation_spectrogram_noisy, f_range=np.array([0, 60]), c_range=np.array([-90, -40])) 55 | plt.title('Modulation spectrogram noisy ECG') 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='am_analysis', 4 | version='0.1', 5 | description='Amplitude Modulation Analysis Module', 6 | url='https://github.com/MuSAELab/amplitude-modulation-analysis-module', 7 | author='Raymundo Cassani and João Monteio', 8 | author_email='raymundo.cassani@gmail.com', 9 | license='MIT', 10 | packages=['am_analysis'], 11 | zip_safe=False) 12 | --------------------------------------------------------------------------------