├── pacpy.egg-info ├── top_level.txt ├── dependency_links.txt ├── requires.txt ├── SOURCES.txt └── PKG-INFO ├── setup.cfg ├── dist └── pacpy-1.0.3.1.zip ├── pacpy ├── __init__.py ├── tests │ ├── exampledata.npy │ ├── test_filt.py │ └── test_pac.py ├── filt.py └── pac.py ├── requirements.txt ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── setup.py └── README.md /pacpy.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | pacpy 2 | -------------------------------------------------------------------------------- /pacpy.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /pacpy.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.9.0 2 | scipy>=0.16.0 3 | 4 | [test] 5 | pytest 6 | -------------------------------------------------------------------------------- /dist/pacpy-1.0.3.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voytekresearch/pacpy/HEAD/dist/pacpy-1.0.3.1.zip -------------------------------------------------------------------------------- /pacpy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import pac 4 | from . import filt 5 | -------------------------------------------------------------------------------- /pacpy/tests/exampledata.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voytekresearch/pacpy/HEAD/pacpy/tests/exampledata.npy -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy == 1.9.2 2 | pytest == 2.7.2 3 | libgfortran == 1.0.0 4 | scipy == 0.16.0 5 | statsmodels == 0.6.1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled py files 2 | *.pyc 3 | # Ignore ipynb checkpoint files 4 | **/.ipynb_checkpoints 5 | # Ignore test cache files 6 | **/.cache 7 | # Ignore files marked as old 8 | *_old -------------------------------------------------------------------------------- /pacpy.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.cfg 2 | setup.py 3 | pacpy/__init__.py 4 | pacpy/filt.py 5 | pacpy/pac.py 6 | pacpy.egg-info/PKG-INFO 7 | pacpy.egg-info/SOURCES.txt 8 | pacpy.egg-info/dependency_links.txt 9 | pacpy.egg-info/requires.txt 10 | pacpy.egg-info/top_level.txt 11 | pacpy/tests/exampledata.npy 12 | pacpy/tests/test_filt.py 13 | pacpy/tests/test_pac.py -------------------------------------------------------------------------------- /pacpy.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: pacpy 3 | Version: 1.0.3.1 4 | Summary: A module to calculate phase-amplitude coupling 5 | Home-page: https://github.com/voytekresearch/pacpy 6 | Author: The Voytek Lab 7 | Author-email: voyteklab@gmail.com 8 | License: MIT 9 | Download-URL: https://github.com/voytekresearch/pacpy/archive/1.0.3.1.tar.gz 10 | Description: 11 | A module to calculate phase-amplitude coupling of neural time series. 12 | 13 | Keywords: neuroscience spectral phase-amplitude 14 | Platform: UNKNOWN 15 | Classifier: Intended Audience :: Science/Research 16 | Classifier: License :: OSI Approved 17 | Classifier: Programming Language :: Python 18 | Classifier: Topic :: Scientific/Engineering 19 | Classifier: Operating System :: Microsoft :: Windows 20 | Classifier: Operating System :: POSIX 21 | Classifier: Operating System :: Unix 22 | Classifier: Operating System :: MacOS 23 | Classifier: Programming Language :: Python :: 2 24 | Classifier: Programming Language :: Python :: 2.6 25 | Classifier: Programming Language :: Python :: 2.7 26 | -------------------------------------------------------------------------------- /pacpy/tests/test_filt.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pacpy 3 | import os 4 | from pacpy.filt import firfls, morletf, firf 5 | 6 | 7 | def test_firf(): 8 | """ 9 | Confirm consistency in FIR filtering 10 | """ 11 | # Load data 12 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 13 | assert np.allclose( 14 | np.sum(np.abs(firf(data, (13, 30)))), 5517466.5857, atol=10 ** -5) 15 | 16 | 17 | def test_firfls(): 18 | """ 19 | Confirm consistency in FIR least-squares filtering 20 | """ 21 | # Load data 22 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 23 | assert np.allclose( 24 | np.sum(np.abs(firfls(data, (13, 30)))), 6020360.04878, atol=10 ** -5) 25 | 26 | 27 | def test_morletf(): 28 | """ 29 | Confirm consistency in morlet wavelet filtering 30 | """ 31 | # Load data 32 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 33 | assert np.allclose( 34 | np.sum(np.abs(morletf(data, 21.5))), 40125678.7918, atol=10 ** -4) 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | notifications: 6 | email: false 7 | 8 | # Setup anaconda to get scipy, and others 9 | before_install: 10 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then 11 | wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; 12 | else 13 | wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 14 | fi 15 | - bash miniconda.sh -b -p $HOME/miniconda 16 | - export PATH="$HOME/miniconda/bin:$PATH" 17 | - hash -r 18 | - conda config --set always_yes yes 19 | - conda update -q conda 20 | # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda 21 | - sudo rm -rf /dev/shm 22 | - sudo ln -s /run/shm /dev/shm 23 | 24 | # Install packages 25 | install: 26 | - conda create -n testenv pip python=$TRAVIS_PYTHON_VERSION 27 | - conda update conda 28 | - source activate testenv 29 | - conda install --file requirements.txt 30 | - pip install . 31 | 32 | # command to run tests 33 | script: py.test pacpy/tests 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Not a Polish bear. 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """pacpy setup script""" 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name='pacpy', 6 | version='1.0.3.1', 7 | description="A module to calculate phase-amplitude coupling", 8 | long_description = """ 9 | A module to calculate phase-amplitude coupling of neural time series. 10 | """, 11 | url='https://github.com/voytekresearch/pacpy', 12 | download_url = 'https://github.com/voytekresearch/pacpy/archive/1.0.3.1.tar.gz', 13 | author='The Voytek Lab', 14 | author_email='voyteklab@gmail.com', 15 | license='MIT', 16 | classifiers=[ 17 | 'Intended Audience :: Science/Research', 18 | 'License :: OSI Approved', 19 | 'Programming Language :: Python', 20 | 'Topic :: Scientific/Engineering', 21 | 'Operating System :: Microsoft :: Windows', 22 | 'Operating System :: POSIX', 23 | 'Operating System :: Unix', 24 | 'Operating System :: MacOS', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.6', 27 | 'Programming Language :: Python :: 2.7' 28 | ], 29 | keywords='neuroscience spectral phase-amplitude', 30 | packages=find_packages(), 31 | install_requires=['numpy>=1.9.0','scipy>=0.16.0'], 32 | extras_require={'test': ['pytest']}, 33 | package_data={ 34 | '': ['tests/*.npy', 'tests/test_*py'], 35 | }, 36 | test_suite="tests" 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pacpy 2 | [![Build Status](https://travis-ci.org/voytekresearch/pacpy.svg)](https://travis-ci.org/voytekresearch/pacpy) 3 | [![Project Status: Inactive – The project has reached a stable, usable state but is no longer being actively developed; support/maintenance will be provided as time allows.](http://www.repostatus.org/badges/latest/inactive.svg)](http://www.repostatus.org/#inactive) 4 | 5 | A module to calculate phase-amplitude coupling in Python. 6 | 7 | ## Note: pactools 8 | Pacpy is no longer actively supported, but it is in a stable state. For an actively maintained package, we recommend [pactools](https://github.com/pactools/pactools). Note that these two packages may give different results when using the default filter parameters. 9 | 10 | ## Demo 11 | 12 | A [Binder](http://mybinder.org) demo, complete with simulated data, can be found [here](https://github.com/srcole/pacpybinder). 13 | 14 | ## Install 15 | 16 | pip install pacpy 17 | 18 | Tested on Linux (Ubuntu 4.10), OS X (10.10.4), and Windows 9. 19 | 20 | ## Dependencies 21 | 22 | - numpy 23 | - scipy 24 | - pytest (optional) 25 | 26 | That is , we assume [Anaconda](https://store.continuum.io/cshop/anaconda/) is installed. 27 | 28 | ## Matlab 29 | 30 | The wrapper for MATLAB can be found at, https://github.com/voytekresearch/pacmat 31 | 32 | ## Usage 33 | 34 | An example of calculating PAC from two simulated voltage signals using the phase-locking value (PLV) method: 35 | 36 | ```python 37 | import numpy as np 38 | from scipy.signal import hilbert 39 | from pacpy.pac import plv 40 | 41 | t = np.arange(0, 10, .001) # Define time array 42 | lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 43 | hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 44 | hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 45 | 46 | plv(lo, hi, (4,8), (80,150)) # Calculate PAC 47 | ``` 48 | ``` 49 | 0.99863308613553081 50 | ``` 51 | -------------------------------------------------------------------------------- /pacpy/filt.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | 4 | from scipy.signal import filtfilt 5 | from scipy.signal import firwin2, firwin 6 | from scipy.signal import morlet 7 | 8 | 9 | def firf(x, f_range, fs=1000, w=3, rmvedge = True): 10 | """ 11 | Filter signal with an FIR filter 12 | *Like fir1 in MATLAB 13 | 14 | x : array-like, 1d 15 | Time series to filter 16 | f_range : (low, high), Hz 17 | Cutoff frequencies of bandpass filter 18 | fs : float, Hz 19 | Sampling rate 20 | w : float 21 | Length of the filter in terms of the number of cycles 22 | of the oscillation whose frequency is the low cutoff of the 23 | bandpass filter 24 | 25 | Returns 26 | ------- 27 | x_filt : array-like, 1d 28 | Filtered time series 29 | """ 30 | 31 | if w <= 0: 32 | raise ValueError( 33 | 'Number of cycles in a filter must be a positive number.') 34 | 35 | nyq = np.float(fs / 2) 36 | if np.any(np.array(f_range) > nyq): 37 | raise ValueError('Filter frequencies must be below nyquist rate.') 38 | 39 | if np.any(np.array(f_range) < 0): 40 | raise ValueError('Filter frequencies must be positive.') 41 | 42 | Ntaps = np.floor(w * fs / f_range[0]) 43 | if len(x) < Ntaps: 44 | raise RuntimeError( 45 | 'Length of filter is loger than data. ' 46 | 'Provide more data or a shorter filter.') 47 | 48 | # Perform filtering 49 | taps = firwin(Ntaps, np.array(f_range) / nyq, pass_zero=False) 50 | x_filt = filtfilt(taps, [1], x) 51 | 52 | if any(np.isnan(x_filt)): 53 | raise RuntimeError( 54 | 'Filtered signal contains nans. Adjust filter parameters.') 55 | 56 | # Remove edge artifacts 57 | if rmvedge: 58 | return _remove_edge(x_filt, Ntaps) 59 | else: 60 | return x_filt 61 | 62 | 63 | def firfls(x, f_range, fs=1000, w=3, tw=.15): 64 | """ 65 | Filter signal with an FIR filter 66 | *Like firls in MATLAB 67 | 68 | x : array-like, 1d 69 | Time series to filter 70 | f_range : (low, high), Hz 71 | Cutoff frequencies of bandpass filter 72 | fs : float, Hz 73 | Sampling rate 74 | w : float 75 | Length of the filter in terms of the number of cycles 76 | of the oscillation whose frequency is the low cutoff of the 77 | bandpass filter 78 | tw : float 79 | Transition width of the filter in normalized frequency space 80 | 81 | Returns 82 | ------- 83 | x_filt : array-like, 1d 84 | Filtered time series 85 | """ 86 | 87 | if w <= 0: 88 | raise ValueError( 89 | 'Number of cycles in a filter must be a positive number.') 90 | 91 | if np.logical_or(tw < 0, tw > 1): 92 | raise ValueError('Transition width must be between 0 and 1.') 93 | 94 | nyq = fs / 2 95 | if np.any(np.array(f_range) > nyq): 96 | raise ValueError('Filter frequencies must be below nyquist rate.') 97 | 98 | if np.any(np.array(f_range) < 0): 99 | raise ValueError('Filter frequencies must be positive.') 100 | 101 | Ntaps = np.floor(w * fs / f_range[0]) 102 | if len(x) < Ntaps: 103 | raise RuntimeError( 104 | 'Length of filter is loger than data. ' 105 | 'Provide more data or a shorter filter.') 106 | 107 | # Characterize desired filter 108 | f = [0, (1 - tw) * f_range[0] / nyq, f_range[0] / nyq, 109 | f_range[1] / nyq, (1 + tw) * f_range[1] / nyq, 1] 110 | m = [0, 0, 1, 1, 0, 0] 111 | if any(np.diff(f) < 0): 112 | raise RuntimeError( 113 | 'Invalid FIR filter parameters.' 114 | 'Please decrease the transition width parameter.') 115 | 116 | # Perform filtering 117 | taps = firwin2(Ntaps, f, m) 118 | x_filt = filtfilt(taps, [1], x) 119 | 120 | if any(np.isnan(x_filt)): 121 | raise RuntimeError( 122 | 'Filtered signal contains nans. Adjust filter parameters.') 123 | 124 | # Remove edge artifacts 125 | return _remove_edge(x_filt, Ntaps) 126 | 127 | 128 | def morletf(x, f0, fs=1000, w=3, s=1, M=None, norm='sss'): 129 | """ 130 | NOTE: This function is not currently ready to be interfaced with pacpy 131 | This is because the frequency input is not a range, which is a big 132 | assumption in how the api is currently designed 133 | 134 | Convolve a signal with a complex wavelet 135 | The real part is the filtered signal 136 | Taking np.abs() of output gives the analytic amplitude 137 | Taking np.angle() of output gives the analytic phase 138 | 139 | x : array 140 | Time series to filter 141 | f0 : float 142 | Center frequency of bandpass filter 143 | Fs : float 144 | Sampling rate 145 | w : float 146 | Length of the filter in terms of the number of 147 | cycles of the oscillation with frequency f0 148 | s : float 149 | Scaling factor for the morlet wavelet 150 | M : integer 151 | Length of the filter. Overrides the f0 and w inputs 152 | norm : string 153 | Normalization method 154 | 'sss' - divide by the sqrt of the sum of squares of points 155 | 'amp' - divide by the sum of amplitudes divided by 2 156 | 157 | Returns 158 | ------- 159 | x_trans : array 160 | Complex time series 161 | """ 162 | 163 | if w <= 0: 164 | raise ValueError( 165 | 'Number of cycles in a filter must be a positive number.') 166 | 167 | if M == None: 168 | M = 2 * s * w * fs / f0 169 | 170 | morlet_f = morlet(M, w=w, s=s) 171 | 172 | if norm == 'sss': 173 | morlet_f = morlet_f / np.sqrt(np.sum(np.abs(morlet_f)**2)) 174 | elif norm == 'abs': 175 | morlet_f = morlet_f / np.sum(np.abs(morlet_f)) * 2 176 | else: 177 | raise ValueError('Not a valid wavelet normalization method.') 178 | 179 | x_filtR = np.convolve(x, np.real(morlet_f), mode='same') 180 | x_filtI = np.convolve(x, np.imag(morlet_f), mode='same') 181 | 182 | # Remove edge artifacts 183 | #x_filtR = _remove_edge(x_filtR, M/2.) 184 | #x_filtI = _remove_edge(x_filtI, M/2.) 185 | 186 | return x_filtR + 1j * x_filtI 187 | 188 | 189 | def _remove_edge(x, N): 190 | """ 191 | Calculate the number of points to remove for edge artifacts 192 | 193 | x : array 194 | time series to remove edge artifacts from 195 | N : int 196 | length of filter 197 | """ 198 | N = int(N) 199 | return x[N:-N] 200 | -------------------------------------------------------------------------------- /pacpy/tests/test_pac.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from scipy.signal import hilbert 4 | import os 5 | import copy 6 | import pacpy 7 | from pacpy.pac import plv, glm, mi_tort, mi_canolty, ozkurt, _trim_edges, otc, _peaktimes, _chunk_time, comodulogram, pa_series, pa_dist 8 | from pacpy.filt import firf, firfls 9 | 10 | 11 | def genPAC1(phabias=.5, flo=5, fhi=100, glm_bias=False): 12 | """ 13 | Generate two signals that have very high PAC 14 | """ 15 | dt = .001 16 | T = 10 17 | t = np.arange(0, T, dt) 18 | 19 | lo = np.sin(t * 2 * np.pi * flo) 20 | hi = np.sin(t * 2 * np.pi * fhi) 21 | 22 | if glm_bias: 23 | hi = hi * (lo + 1) 24 | else: 25 | pha = np.angle(hilbert(lo)) 26 | hi[pha > -np.pi + phabias] = 0 27 | 28 | return lo, hi 29 | 30 | 31 | def genPAC0(flo=5, fhi=100): 32 | """ 33 | Generate two signals that have very low PAC 34 | """ 35 | dt = .001 36 | T = 10 37 | t = np.arange(0, T, dt) 38 | 39 | lo = np.sin(t * 2 * np.pi * flo) 40 | hi = np.sin(t * 2 * np.pi * fhi) 41 | return lo, hi 42 | 43 | 44 | def test_plv(): 45 | """ 46 | Test PAC function: PLV. 47 | 1. Confirm consistency of output with example data 48 | 2. Confirm consistency of output with example data using firfls filter 49 | 3. Confirm PAC=1 when expected 50 | 4. Confirm PAC=0 when expected 51 | """ 52 | # Load data 53 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 54 | assert np.allclose( 55 | plv(data, data, (13, 30), (80, 200)), 0.25114, atol=10 ** -5) 56 | assert np.allclose( 57 | plv(data, data, (13, 30), (80, 200), filterfn=firfls), 0.24715, atol=10 ** -5) 58 | 59 | # Test that the PLV function outputs close to 0 and 1 when expected 60 | lo, hi = genPAC1() 61 | assert plv(lo, hi, (4, 6), (90, 110)) > 0.99 62 | 63 | lo, hi = genPAC0() 64 | assert plv(lo, hi, (4, 6), (90, 110)) < 0.01 65 | 66 | # Test that Filterfn = False works as expected 67 | datalo = firf(data, (13, 30)) 68 | datahi = firf(data, (80, 200)) 69 | datahiamp = np.abs(hilbert(datahi)) 70 | datahiamplo = firf(datahiamp, (13, 30)) 71 | pha1 = np.angle(hilbert(datalo)) 72 | pha2 = np.angle(hilbert(datahiamplo)) 73 | pha1, pha2 = _trim_edges(pha1, pha2) 74 | assert np.allclose( 75 | plv(pha1, pha2, (13, 30), (80, 200), filterfn=False), 76 | plv(data, data, (13, 30), (80, 200)), atol=10 ** -5) 77 | 78 | 79 | def test_glm(): 80 | """ 81 | Test PAC function: GLM 82 | 1. Confirm consistency of output with example data 83 | 2. Confirm PAC=1 when expected 84 | 3. Confirm PAC=0 when expected 85 | """ 86 | # Load data 87 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 88 | assert np.allclose( 89 | glm(data, data, (13, 30), (80, 200)), 0.03243, atol=10 ** -5) 90 | 91 | # Test that the GLM function outputs close to 0 and 1 when expected 92 | lo, hi = genPAC1(glm_bias=True) 93 | assert glm(lo, hi, (4, 6), (90, 110)) > 0.99 94 | 95 | lo, hi = genPAC0() 96 | assert glm(lo, hi, (4, 6), (90, 110)) < 0.01 97 | 98 | 99 | def test_mi_tort(): 100 | """ 101 | Test PAC function: Tort MI 102 | 1. Confirm consistency of output with example data 103 | 2. Confirm PAC=1 when expected 104 | 3. Confirm PAC=0 when expected 105 | """ 106 | # Load data 107 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 108 | assert np.allclose( 109 | mi_tort(data, data, (13, 30), (80, 200)), 0.00363, atol=10 ** -5) 110 | 111 | # Test that the Tort MI function outputs close to 0 and 1 when expected 112 | lo, hi = genPAC1(phabias=.2, fhi=300) 113 | assert mi_tort(lo, hi, (4, 6), (100, 400)) > 0.7 114 | 115 | lo, hi = genPAC0() 116 | assert mi_tort(lo, hi, (4, 6), (100, 400)) < 10 ** -5 117 | 118 | 119 | def test_mi_canolty(): 120 | """ 121 | Test PAC function: Canolty MI 122 | 1. Confirm consistency of output with example data 123 | """ 124 | # Load data 125 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 126 | np.random.seed(0) 127 | assert np.allclose( 128 | mi_canolty(data, data, (13, 30), (80, 200)), 22.08946, atol=10 ** -5) 129 | 130 | 131 | def test_ozkurt(): 132 | """ 133 | Test PAC function: Ozkurt 134 | 1. Confirm consistency of output with example data 135 | 2. Confirm PAC=1 when expected 136 | 3. Confirm PAC=0 when expected 137 | """ 138 | # Load data 139 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 140 | assert np.allclose( 141 | ozkurt(data, data, (13, 30), (80, 200)), 0.07658, atol=10 ** -5) 142 | 143 | # Test that the Ozkurt PAC function outputs close to 0 and 1 when expected 144 | lo, hi = genPAC1(phabias=.2, fhi=300) 145 | hif = firf(hi, (100, 400)) 146 | amp = np.abs(hilbert(hif)) 147 | weight = (np.sqrt(len(amp)) * np.sqrt(np.sum(amp ** 2))) / np.sum(amp) 148 | assert ozkurt(lo, hi, (4, 6), (100, 400)) * weight > 0.98 149 | 150 | lo, hi = genPAC0() 151 | assert ozkurt(lo, hi, (4, 6), (100, 400)) < 0.01 152 | 153 | 154 | def test_otc(): 155 | """ 156 | Test PAC function: OTC 157 | """ 158 | # Load data 159 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 160 | 161 | # Confirm consistency in result 162 | t_modsig = (-.5, .5) 163 | fs = 1000. 164 | f_hi = (80, 200) 165 | f_step = 4 166 | pac, tf, a_events, mod_sig = otc( 167 | data, f_hi, f_step, fs=fs, t_modsig=t_modsig) 168 | assert np.allclose(pac, 235.02888, atol=10 ** -5) 169 | 170 | # Confirm correct shapes in outputs 171 | assert np.shape(tf) == ((f_hi[1] - f_hi[0]) / f_step, len(data)) 172 | for i in range(len(a_events)): 173 | assert np.max(a_events[i]) <= len(data) 174 | assert np.shape(mod_sig) == ( 175 | (f_hi[1] - f_hi[0]) / f_step, len(np.arange(t_modsig[0], t_modsig[1], 1 / fs))) 176 | 177 | 178 | def test_peaktimes(): 179 | """ 180 | Test OTC helper function: _peaktimes 181 | """ 182 | # Load data 183 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 184 | 185 | # Confirm functionality 186 | assert _peaktimes(data[:1000], prc=99) == 344 187 | assert len(_peaktimes(data[:10000], prc=99)) == 11 188 | 189 | 190 | def test_chunktime(): 191 | """ 192 | Test OTC helper function: _chunk_time 193 | """ 194 | assert np.array_equal(_chunk_time( 195 | [5, 6, 7, 8, 10, 55, 56], samp_buffer=0), np.array([[5, 8], [10, 10], [55, 56]])) 196 | assert np.array_equal(_chunk_time( 197 | [5, 6, 7, 8, 10, 55, 56], samp_buffer=2), np.array([[5, 10], [55, 56]])) 198 | 199 | 200 | def test_comod(): 201 | """ 202 | Test comodulogram function 203 | """ 204 | # Load data 205 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 206 | p_range = [10, 21] 207 | a_range = [50, 150] 208 | dp = 5 209 | da = 50 210 | a = comodulogram(data, data, p_range, a_range, dp, da) 211 | assert np.allclose(a[0][0], 0.00315, atol=10 ** -5) 212 | assert np.shape(a) == (len(np.arange(p_range[0], p_range[1], dp)), len( 213 | np.arange(a_range[0], a_range[1], da))) 214 | 215 | 216 | def test_paseries(): 217 | """ 218 | Test calculation of phase and amplitude time series 219 | """ 220 | # Load data 221 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 222 | 223 | # Confirm returns correct size 224 | p, a = pa_series(data, data, (13, 30), (80, 200)) 225 | assert np.allclose(np.mean(a), 9.41637, atol=10 ** -5) 226 | assert np.allclose(p[0], -1.83601, atol=10 ** -5) 227 | 228 | 229 | def test_padist(): 230 | """ 231 | Test calculation of amplitude distribution as a function of phase 232 | """ 233 | # Load data 234 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 235 | 236 | np.random.seed(0) 237 | Nbins = np.random.randint(2, 20) 238 | pha, amp = pa_series(data, data, (13, 30), (80, 200)) 239 | boundaries, dist = pa_dist(pha, amp, Nbins=Nbins) 240 | assert len(dist) == Nbins 241 | assert len(boundaries) == Nbins 242 | 243 | # Confirm consistency 244 | _, dist = pa_dist(pha, amp, Nbins=10) 245 | assert np.allclose(dist[0], 9.90227, atol=10 ** -5) 246 | 247 | 248 | def test_raiseinputerrors(): 249 | """ 250 | Confirm that ValueErrors from dumb user input are raised 251 | """ 252 | # Load data 253 | data = np.load(os.path.dirname(pacpy.__file__) + '/tests/exampledata.npy') 254 | data2 = copy.copy(data) 255 | data2[-1] = np.nan 256 | 257 | with pytest.raises(ValueError) as excinfo: 258 | plv(data, data[:-1], (13, 30), (80, 200)) 259 | assert 'same length' in str(excinfo.value) 260 | 261 | with pytest.raises(ValueError) as excinfo: 262 | plv(data, data2, (13, 30), (80, 200)) 263 | assert 'NaNs' in str(excinfo.value) 264 | 265 | with pytest.raises(ValueError) as excinfo: 266 | plv(data, data, (13, 30, 31), (80, 200)) 267 | assert 'two elements' in str(excinfo.value) 268 | 269 | with pytest.raises(ValueError) as excinfo: 270 | plv(data, data, (13, 30), (80, 200, 201)) 271 | assert 'two elements' in str(excinfo.value) 272 | 273 | with pytest.raises(ValueError) as excinfo: 274 | plv(data, data, (-13, 30), (80, 200)) 275 | assert 'must be > 0' in str(excinfo.value) 276 | 277 | with pytest.raises(ValueError) as excinfo: 278 | plv(data, data, (13, 30), (-80, 200)) 279 | assert 'must be > 0' in str(excinfo.value) 280 | 281 | with pytest.raises(ValueError) as excinfo: 282 | mi_tort(data, data, (13, 30), (80, 200), Nbins=1) 283 | assert 'integer >1' in str(excinfo.value) 284 | 285 | with pytest.raises(ValueError) as excinfo: 286 | mi_tort(data, data, (13, 30), (80, 200), Nbins=8.8) 287 | assert 'integer >1' in str(excinfo.value) 288 | 289 | with pytest.raises(ValueError) as excinfo: 290 | otc(data, (80, 200), -1) 291 | assert 'positive number' in str(excinfo.value) 292 | 293 | with pytest.raises(ValueError) as excinfo: 294 | otc(data, (80, 200), 4, t_modsig=(.5, -.5)) 295 | assert 'Invalid time range' in str(excinfo.value) 296 | 297 | with pytest.raises(ValueError) as excinfo: 298 | _peaktimes(data, prc=101) 299 | assert '0 and 100' in str(excinfo.value) 300 | 301 | with pytest.raises(ValueError) as excinfo: 302 | _chunk_time(data, samp_buffer=-1) 303 | assert 'positive number' in str(excinfo.value) 304 | 305 | with pytest.raises(ValueError) as excinfo: 306 | _chunk_time(data, samp_buffer=2.5) 307 | assert 'integer' in str(excinfo.value) 308 | -------------------------------------------------------------------------------- /pacpy/pac.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions to calculate phase-amplitude coupling. 4 | """ 5 | from __future__ import division 6 | import numpy as np 7 | from scipy.signal import hilbert 8 | from scipy.stats.mstats import zscore 9 | from pacpy.filt import firf, morletf 10 | 11 | 12 | def _x_sanity(lo=None, hi=None): 13 | if lo is not None: 14 | if np.any(np.isnan(lo)): 15 | raise ValueError("lo contains NaNs") 16 | 17 | if hi is not None: 18 | if np.any(np.isnan(hi)): 19 | raise ValueError("hi contains NaNs") 20 | 21 | if (hi is not None) and (lo is not None): 22 | if lo.size != hi.size: 23 | raise ValueError("lo and hi must be the same length") 24 | 25 | 26 | def _range_sanity(f_lo=None, f_hi=None): 27 | if f_lo is not None: 28 | if len(f_lo) != 2: 29 | raise ValueError("f_lo must contain two elements") 30 | 31 | if f_lo[0] < 0: 32 | raise ValueError("Elements in f_lo must be > 0") 33 | 34 | if f_hi is not None: 35 | if len(f_hi) != 2: 36 | raise ValueError("f_hi must contain two elements") 37 | if f_hi[0] < 0: 38 | raise ValueError("Elements in f_hi must be > 0") 39 | 40 | 41 | def plv(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 42 | filterfn=None, filter_kwargs=None): 43 | """ 44 | Calculate PAC using the phase-locking value (PLV) method from prefiltered 45 | signals 46 | 47 | Parameters 48 | ---------- 49 | lo : array-like, 1d 50 | The low frequency time-series to use as the phase component 51 | hi : array-like, 1d 52 | The high frequency time-series to use as the amplitude component 53 | f_lo : (low, high), Hz 54 | The low frequency filtering range 55 | f_hi : (low, high), Hz 56 | The low frequency filtering range 57 | w_lo : float 58 | Number of cycles for the filter order of the low band-pass filter 59 | w_hi : float 60 | Number of cycles for the filter order of the low band-pass filter 61 | fs : float 62 | The sampling rate (default = 1000Hz) 63 | filterfn : function, False 64 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 65 | 66 | False activates 'EXPERT MODE'. 67 | - DO NOT USE THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING! 68 | - In expert mode the user needs to filter the data AND apply the 69 | hilbert transform. 70 | - This requires that 'lo' be the phase time series of the low-bandpass 71 | filtered signal, and 'hi' be the phase time series of the low-bandpass 72 | of the amplitude of the high-bandpass of the original signal. 73 | filter_kwargs : dict 74 | Keyword parameters to pass to `filterfn(.)` 75 | 76 | Returns 77 | ------- 78 | pac : scalar 79 | PAC value 80 | 81 | Usage 82 | ----- 83 | >>> import numpy as np 84 | >>> from scipy.signal import hilbert 85 | >>> from pacpy.pac import plv 86 | >>> t = np.arange(0, 10, .001) # Define time array 87 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 88 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 89 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 90 | >>> plv(lo, hi, (4,8), (80,150)) # Calculate PAC 91 | 0.99863308613553081 92 | """ 93 | 94 | lo, hi = pa_series(lo, hi, f_lo, f_hi, fs=fs, w_lo=w_lo, w_hi=w_hi, 95 | filterfn=filterfn, filter_kwargs=filter_kwargs, hi_phase=True) 96 | 97 | # Calculate PLV 98 | pac = np.abs(np.mean(np.exp(1j * (lo - hi)))) 99 | 100 | return pac 101 | 102 | 103 | def _trim_edges(lo, hi): 104 | """ 105 | Remove extra edge artifact from the signal with the shorter filter 106 | so that its time series is identical to that of the filtered signal 107 | with a longer filter. 108 | """ 109 | 110 | if len(lo) == len(hi): 111 | return lo, hi # Die early if there's nothing to do. 112 | elif len(lo) < len(hi): 113 | Ndiff = len(hi) - len(lo) 114 | if Ndiff % 2 != 0: 115 | raise ValueError( 116 | 'Difference in filtered signal lengths should be even') 117 | hi = hi[np.int(Ndiff / 2):np.int(-Ndiff / 2)] 118 | else: 119 | Ndiff = len(lo) - len(hi) 120 | if Ndiff % 2 != 0: 121 | raise ValueError( 122 | 'Difference in filtered signal lengths should be even') 123 | lo = lo[np.int(Ndiff / 2):np.int(-Ndiff / 2)] 124 | 125 | return lo, hi 126 | 127 | 128 | def mi_tort(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 129 | Nbins=20, filterfn=None, filter_kwargs=None): 130 | """ 131 | Calculate PAC using the modulation index method from prefiltered 132 | signals 133 | 134 | Parameters 135 | ---------- 136 | lo : array-like, 1d 137 | The low frequency time-series to use as the phase component 138 | hi : array-like, 1d 139 | The high frequency time-series to ue as the amplitude component 140 | f_lo : (low, high), Hz 141 | The low frequency filtering ranges 142 | f_hi : (low, high), Hz 143 | The low frequency filtering range 144 | w_lo : float 145 | Number of cycles for the filter order of the low band-pass filter 146 | w_hi : float 147 | Number of cycles for the filter order of the low band-pass filter 148 | fs : float 149 | The sampling rate (default = 1000Hz) 150 | filterfn : functional 151 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 152 | 153 | False activates 'EXPERT MODE'. 154 | - DO NOT USE THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING! 155 | - In expert mode the user needs to filter the data AND apply the 156 | hilbert transform. 157 | - This requires that 'lo' be the phase time series of the low-bandpass 158 | filtered signal, and 'hi' be the amplitude time series of the high- 159 | bandpass of the original signal. 160 | filter_kwargs : dict 161 | Keyword parameters to pass to `filterfn(.)` 162 | Nbins : int 163 | Number of bins to split up the low frequency oscillation cycle 164 | 165 | Returns 166 | ------- 167 | pac : scalar 168 | PAC value 169 | 170 | Usage 171 | ----- 172 | >>> import numpy as np 173 | >>> from scipy.signal import hilbert 174 | >>> from pacpy.pac import mi_tort 175 | >>> t = np.arange(0, 10, .001) # Define time array 176 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 177 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 178 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 179 | >>> mi_tort(lo, hi, (4,8), (80,150)) # Calculate PAC 180 | 0.34898478944110811 181 | """ 182 | 183 | # Arg check 184 | if np.logical_or(Nbins < 2, Nbins != int(Nbins)): 185 | raise ValueError( 186 | 'Number of bins in the low frequency oscillation cycle' 187 | 'must be an integer >1.') 188 | 189 | lo, hi = pa_series(lo, hi, f_lo, f_hi, fs=fs, w_lo=w_lo, w_hi=w_hi, 190 | filterfn=filterfn, filter_kwargs=filter_kwargs) 191 | 192 | # Convert the phase time series from radians to degrees 193 | phadeg = np.degrees(lo) 194 | 195 | # Calculate PAC 196 | binsize = 360 / Nbins 197 | phase_lo = np.arange(-180, 180, binsize) 198 | mean_amp = np.zeros(len(phase_lo)) 199 | for b in range(len(phase_lo)): 200 | phaserange = np.logical_and(phadeg >= phase_lo[b], 201 | phadeg < (phase_lo[b] + binsize)) 202 | mean_amp[b] = np.mean(hi[phaserange]) 203 | 204 | p_j = np.zeros(len(phase_lo)) 205 | for b in range(len(phase_lo)): 206 | p_j[b] = mean_amp[b] / sum(mean_amp) 207 | 208 | h = -np.sum(p_j * np.log10(p_j)) 209 | h_max = np.log10(Nbins) 210 | pac = (h_max - h) / h_max 211 | 212 | return pac 213 | 214 | 215 | def _ols(y, X): 216 | """Custom OLS (to minimize outside dependecies)""" 217 | 218 | dummy = np.repeat(1.0, X.shape[0]) 219 | X = np.hstack([X, dummy[:, np.newaxis]]) 220 | 221 | beta_hat, resid, _, _ = np.linalg.lstsq(X, y) 222 | y_hat = np.dot(X, beta_hat) 223 | 224 | return y_hat, beta_hat 225 | 226 | 227 | def glm(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 228 | filterfn=None, filter_kwargs=None): 229 | """ 230 | Calculate PAC using the generalized linear model (GLM) method 231 | 232 | Parameters 233 | ---------- 234 | lo : array-like, 1d 235 | The low frequency time-series to use as the phase component 236 | hi : array-like, 1d 237 | The high frequency time-series to use as the amplitude component 238 | f_lo : (low, high), Hz 239 | The low frequency filtering range 240 | f_high : (low, high), Hz 241 | The low frequency filtering range 242 | fs : float 243 | The sampling rate (default = 1000Hz) 244 | w_lo : float 245 | Number of cycles for the filter order of the low band-pass filter 246 | w_hi : float 247 | Number of cycles for the filter order of the low band-pass filter 248 | filterfn : functional 249 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 250 | 251 | False activates 'EXPERT MODE'. 252 | - DO NOT USE THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING! 253 | - In expert mode the user needs to filter the data AND apply the 254 | hilbert transform. 255 | - This requires that 'lo' be the phase time series of the low-bandpass 256 | filtered signal, and 'hi' be the amplitude time series of the high- 257 | bandpass of the original signal. 258 | filter_kwargs : dict 259 | Keyword parameters to pass to `filterfn(.)` 260 | 261 | Returns 262 | ------- 263 | pac : scalar 264 | PAC value 265 | 266 | Usage 267 | ----- 268 | >>> import numpy as np 269 | >>> from scipy.signal import hilbert 270 | >>> from pacpy.pac import glm 271 | >>> t = np.arange(0, 10, .001) # Define time array 272 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 273 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 274 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 275 | >>> glm(lo, hi, (4,8), (80,150)) # Calculate PAC 276 | 0.69090396896138917 277 | """ 278 | 279 | lo, hi = pa_series(lo, hi, f_lo, f_hi, fs=fs, w_lo=w_lo, w_hi=w_hi, 280 | filterfn=filterfn, filter_kwargs=filter_kwargs) 281 | 282 | # First prepare GLM 283 | y = hi 284 | X_pre = np.vstack((np.cos(lo), np.sin(lo))) 285 | X = X_pre.T 286 | y_hat, beta_hat = _ols(y, X) 287 | resid = y - y_hat 288 | 289 | # Calculate PAC from GLM residuals 290 | pac = 1 - np.sum(resid ** 2) / np.sum( 291 | (hi - np.mean(hi)) ** 2) 292 | 293 | return pac 294 | 295 | 296 | def mi_canolty(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 297 | filterfn=None, filter_kwargs=None, n_surr=100): 298 | """ 299 | Calculate PAC using the modulation index (MI) method defined in Canolty, 300 | 2006 301 | 302 | Parameters 303 | ---------- 304 | lo : array-like, 1d 305 | The low frequency time-series to use as the phase component 306 | hi : array-like, 1d 307 | The high frequency time-series to use as the amplitude component 308 | f_lo : (low, high), Hz 309 | The low frequency filtering range 310 | f_hi : (low, high), Hz 311 | The low frequency filtering range 312 | fs : float 313 | The sampling rate (default = 1000Hz) 314 | w_lo : float 315 | Number of cycles for the filter order of the low band-pass filter 316 | w_hi : float 317 | Number of cycles for the filter order of the low band-pass filter 318 | filterfn : functional 319 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 320 | 321 | False activates 'EXPERT MODE'. 322 | - DO NOT USE THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING! 323 | - In expert mode the user needs to filter the data AND apply the 324 | hilbert transform. 325 | - This requires that 'lo' be the phase time series of the low-bandpass 326 | filtered signal, and 'hi' be the amplitude time series of the high- 327 | bandpass of the original signal. 328 | filter_kwargs : dict 329 | Keyword parameters to pass to `filterfn(.)` 330 | n_surr : int 331 | Number of surrogate tests to run to calculate normalized MI 332 | 333 | Returns 334 | ------- 335 | pac : scalar 336 | PAC value 337 | 338 | Usage 339 | ----- 340 | >>> import numpy as np 341 | >>> from scipy.signal import hilbert 342 | >>> from pacpy.pac import mi_canolty 343 | >>> t = np.arange(0, 10, .001) # Define time array 344 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 345 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 346 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 347 | >>> mi_canolty(lo, hi, (4,8), (80,150)) # Calculate PAC 348 | 1.1605177063713188 349 | """ 350 | 351 | lo, hi = pa_series(lo, hi, f_lo, f_hi, fs=fs, w_lo=w_lo, w_hi=w_hi, 352 | filterfn=filterfn, filter_kwargs=filter_kwargs) 353 | 354 | # Calculate modulation index 355 | pac = np.abs(np.mean(hi * np.exp(1j * lo))) 356 | 357 | # Calculate surrogate MIs 358 | pacS = np.zeros(n_surr) 359 | 360 | loj = np.exp(1j * lo) 361 | for s in range(n_surr): 362 | loS = np.roll(loj, np.random.randint(len(lo))) 363 | pacS[s] = np.abs(np.mean(hi * loS)) 364 | 365 | # Return z-score of observed PAC compared to null distribution 366 | return (pac - np.mean(pacS)) / np.std(pacS) 367 | 368 | 369 | def ozkurt(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 370 | filterfn=None, filter_kwargs=None): 371 | """ 372 | Calculate PAC using the method defined in Ozkurt & Schnitzler, 2011 373 | 374 | Parameters 375 | ---------- 376 | lo : array-like, 1d 377 | The low frequency time-series to use as the phase component 378 | hi : array-like, 1d 379 | The high frequency time-series to use as the amplitude component 380 | f_lo : (low, high), Hz 381 | The low frequency filtering range 382 | f_hi : (low, high), Hz 383 | The low frequency filtering range 384 | w_lo : float 385 | Number of cycles for the filter order of the low band-pass filter 386 | w_hi : float 387 | Number of cycles for the filter order of the low band-pass filter 388 | fs : float 389 | The sampling rate (default = 1000Hz) 390 | filterfn : functional 391 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 392 | 393 | False activates 'EXPERT MODE'. 394 | - DO NOT USE THIS FLAG UNLESS YOU KNOW WHAT YOU ARE DOING! 395 | - In expert mode the user needs to filter the data AND apply the 396 | hilbert transform. 397 | - This requires that 'lo' be the phase time series of the low-bandpass 398 | filtered signal, and 'hi' be the amplitude time series of the high- 399 | bandpass of the original signal. 400 | filter_kwargs : dict 401 | Keyword parameters to pass to `filterfn(.)` 402 | 403 | Returns 404 | ------- 405 | pac : scalar 406 | PAC value 407 | 408 | Usage 409 | ----- 410 | >>> import numpy as np 411 | >>> from scipy.signal import hilbert 412 | >>> from pacpy.pac import ozkurt 413 | >>> t = np.arange(0, 10, .001) # Define time array 414 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 415 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 416 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 417 | >>> ozkurt(lo, hi, (4,8), (80,150)) # Calculate PAC 418 | 0.48564417921240238 419 | """ 420 | 421 | lo, hi = pa_series(lo, hi, f_lo, f_hi, fs=fs, w_lo=w_lo, w_hi=w_hi, 422 | filterfn=filterfn, filter_kwargs=filter_kwargs) 423 | 424 | # Calculate PAC 425 | pac = np.abs(np.sum(hi * np.exp(1j * lo))) / \ 426 | (np.sqrt(len(lo)) * np.sqrt(np.sum(hi**2))) 427 | return pac 428 | 429 | 430 | def otc(x, f_hi, f_step, fs=1000, 431 | w=3, event_prc=95, t_modsig=None, t_buffer=.01): 432 | """ 433 | Calculate the oscillation-triggered coupling measure of phase-amplitude 434 | coupling from Dvorak, 2014. 435 | 436 | Parameters 437 | ---------- 438 | x : array-like, 1d 439 | The time series 440 | f_hi : (low, high), Hz 441 | The low frequency filtering range 442 | f_step : float, Hz 443 | The width of each frequency bin in the time-frequency representation 444 | fs : float 445 | Sampling rate 446 | w : float 447 | Length of the filter in terms of the number of cycles of the 448 | oscillation whose frequency is the center of the bandpass filter 449 | event_prc : float (in range 0-100) 450 | The percentile threshold of the power signal of an oscillation 451 | for an event to be declared 452 | t_modsig : (min, max) 453 | Time (seconds) around an event to extract to define the modulation 454 | signal 455 | t_buffer : float 456 | Minimum time (seconds) in between high frequency events 457 | 458 | Returns 459 | ------- 460 | pac : float 461 | phase-amplitude coupling value 462 | tf : 2-dimensional array 463 | time-frequency representation of input signal 464 | a_events : array 465 | samples at which a high frequency event occurs 466 | mod_sig : array 467 | modulation signal (see Dvorak, 2014) 468 | 469 | Usage 470 | ----- 471 | >>> import numpy as np 472 | >>> from scipy.signal import hilbert 473 | >>> from pacpy.pac import otc 474 | >>> t = np.arange(0, 10, .001) # Define time array 475 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 476 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 477 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 478 | >>> pac, _, _, _ = otc(lo + hi, (80,150), 4) # Calculate PAC 479 | >>> print pac 480 | 2.1324570402314196 481 | """ 482 | 483 | # Arg check 484 | _x_sanity(x, None) 485 | _range_sanity(None, f_hi) 486 | # Set default time range for modulatory signal 487 | if t_modsig is None: 488 | t_modsig = (-1, 1) 489 | if f_step <= 0: 490 | raise ValueError('Frequency band width must be a positive number.') 491 | if t_modsig[0] > t_modsig[1]: 492 | raise ValueError('Invalid time range for modulation signal.') 493 | 494 | # Calculate the time-frequency representation 495 | f0s = np.arange(f_hi[0], f_hi[1], f_step) 496 | tf = _morletT(x, f0s, w=w, fs=fs) 497 | 498 | # Find the high frequency activity event times 499 | F = len(f0s) 500 | a_events = np.zeros(F, dtype=object) 501 | for f in range(F): 502 | a_events[f] = _peaktimes( 503 | zscore(np.abs(tf[f])), prc=event_prc, t_buffer=t_buffer) 504 | 505 | # Calculate the modulation signal 506 | samp_modsig = np.arange(t_modsig[0] * fs, t_modsig[1] * fs) 507 | samp_modsig = samp_modsig.astype(int) 508 | S = len(samp_modsig) 509 | mod_sig = np.zeros([F, S]) 510 | 511 | # For each frequency in the time-frequency representation, calculate a 512 | # modulation signal 513 | for f in range(F): 514 | # Exclude high frequency events that are too close to the signal 515 | # boundaries to extract an entire modulation signal 516 | mask = np.ones(len(a_events[f]), dtype=bool) 517 | mask[a_events[f] <= samp_modsig[-1]] = False 518 | mask[a_events[f] >= (len(x) - samp_modsig[-1])] = False 519 | a_events[f] = a_events[f][mask] 520 | 521 | # Calculate the average LFP around each high frequency event 522 | E = len(a_events[f]) 523 | for e in range(E): 524 | cur_ecog = x[a_events[f][e] + samp_modsig] 525 | mod_sig[f] = mod_sig[f] + cur_ecog / E 526 | 527 | # Calculate modulation strength, the range of the modulation signal 528 | mod_strength = np.zeros(F) 529 | for f in range(F): 530 | mod_strength = np.max(mod_sig[f]) - np.min(mod_sig[f]) 531 | 532 | # Calculate PAC 533 | pac = np.max(mod_strength) 534 | 535 | return pac, tf, a_events, mod_sig 536 | 537 | 538 | def _peaktimes(x, prc=95, t_buffer=.01, fs=1000): 539 | """ 540 | Calculate event times for which the power signal x peaks 541 | 542 | Parameters 543 | ---------- 544 | x : array 545 | Time series of power 546 | prc : float (in range 0-100) 547 | The percentile threshold of x for an event to be declares 548 | t_buffer : float 549 | Minimum time (seconds) in between events 550 | fs : float 551 | Sampling rate 552 | """ 553 | if np.logical_or(prc < 0, prc >= 100): 554 | raise ValueError('Percentile threshold must be between 0 and 100.') 555 | 556 | samp_buffer = np.int(np.round(t_buffer * fs)) 557 | hi = x > np.percentile(x, prc) 558 | event_intervals = _chunk_time(hi, samp_buffer=samp_buffer) 559 | E = np.int(np.size(event_intervals) / 2) 560 | events = np.zeros(E, dtype=object) 561 | 562 | for e in range(E): 563 | temp = x[np.arange(event_intervals[e][0], event_intervals[e][1] + 1)] 564 | events[e] = event_intervals[e][0] + np.argmax(temp) 565 | 566 | return events 567 | 568 | 569 | def _chunk_time(x, samp_buffer=0): 570 | """ 571 | Define continuous chunks of integers 572 | 573 | Parameters 574 | ---------- 575 | x : array 576 | Array of integers 577 | samp_buffer : int 578 | Minimum number of samples between chunks 579 | 580 | Returns 581 | ------- 582 | chunks : array (#chunks x 2) 583 | List of the sample bounds for each chunk 584 | """ 585 | if samp_buffer < 0: 586 | raise ValueError( 587 | 'Buffer between signal peaks must be a positive number') 588 | if samp_buffer != int(samp_buffer): 589 | raise ValueError('Number of samples must be an integer') 590 | 591 | if type(x[0]) == np.bool_: 592 | Xs = np.arange(len(x)) 593 | x = Xs[x] 594 | X = len(x) 595 | 596 | cur_start = x[0] 597 | cur_samp = x[0] 598 | Nchunk = 0 599 | chunks = [] 600 | for i in range(1, X): 601 | if x[i] > (cur_samp + samp_buffer + 1): 602 | if Nchunk == 0: 603 | chunks = [cur_start, cur_samp] 604 | else: 605 | chunks = np.vstack([chunks, [cur_start, cur_samp]]) 606 | 607 | Nchunk = Nchunk + 1 608 | cur_start = x[i] 609 | 610 | cur_samp = x[i] 611 | 612 | # Add final row to chunk 613 | if Nchunk == 0: 614 | chunks = [[cur_start, cur_samp]] 615 | else: 616 | chunks = np.vstack([chunks, [cur_start, cur_samp]]) 617 | 618 | return chunks 619 | 620 | 621 | def _morletT(x, f0s, w=3, fs=1000, s=1): 622 | """ 623 | Calculate the time-frequency representation of the signal 'x' over the 624 | frequencies in 'f0s' using morlet wavelets 625 | 626 | Parameters 627 | ---------- 628 | x : array 629 | time series 630 | f0s : array 631 | frequency axis 632 | w : float 633 | Length of the filter in terms of the number of cycles 634 | of the oscillation whose frequency is the center of the 635 | bandpass filter 636 | Fs : float 637 | Sampling rate 638 | s : float 639 | Scaling factor 640 | 641 | Returns 642 | ------- 643 | mwt : 2-D array 644 | time-frequency representation of signal x 645 | """ 646 | if w <= 0: 647 | raise ValueError( 648 | 'Number of cycles in a filter must be a positive number.') 649 | 650 | T = len(x) 651 | F = len(f0s) 652 | mwt = np.zeros([F, T], dtype=complex) 653 | for f in range(F): 654 | mwt[f] = morletf(x, f0s[f], fs=fs, w=w, s=s) 655 | 656 | return mwt 657 | 658 | 659 | def comodulogram(lo, hi, p_range, a_range, dp, da, fs=1000, w_lo=3, w_hi=3, 660 | pac_method='mi_tort', 661 | filterfn=None, filter_kwargs=None): 662 | """ 663 | Calculate PAC for many small frequency bands 664 | 665 | Parameters 666 | ---------- 667 | lo : array-like, 1d 668 | The low frequency time-series to use as the phase component 669 | hi : array-like, 1d 670 | The high frequency time-series to use as the amplitude component 671 | p_range : (low, high), Hz 672 | The low frequency filtering range 673 | a_range : (low, high), Hz 674 | The high frequency filtering range 675 | dp : float, Hz 676 | Width of the low frequency filtering range for each PAC calculation 677 | da : float, Hz 678 | Width of the high frequency filtering range for each PAC calculation 679 | fs : float 680 | The sampling rate (default = 1000Hz) 681 | pac_method : string 682 | Method to calculate PAC. 683 | 'mi_tort' - See Tort, 2008 684 | 'plv' - See Penny, 2008 685 | 'glm' - See Penny, 2008 686 | 'mi_canolty' - See Canolty, 2006 687 | 'ozkurt' - See Ozkurt & Schnitzler, 2011 688 | filterfn : function 689 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 690 | In this case, filter functions should NOT remove edge artifact. Edge 691 | artifacts will be removed based on the frequency and 'w_lo' parameter 692 | filter_kwargs : dict 693 | Keyword parameters to pass to `filterfn(.)` 694 | 695 | Returns 696 | ------- 697 | comod : array-like, 2d 698 | Matrix of phase-amplitude coupling values for each combination of the 699 | phase frequency bin and the amplitude frequency bin 700 | 701 | Usage 702 | ----- 703 | >>> import numpy as np 704 | >>> from scipy.signal import hilbert 705 | >>> from pacpy.pac import comodulogram 706 | >>> t = np.arange(0, 10, .001) # Define time array 707 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 708 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 709 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 710 | >>> comod = comodulogram(lo, hi, (5,25), (75,175), 10, 50) # Calculate PAC 711 | >>> print comod 712 | [[ 0.32708628 0.32188585] 713 | [ 0.3295994 0.32439953]] 714 | """ 715 | 716 | # Arg check 717 | _x_sanity(lo, hi) 718 | _range_sanity(p_range, a_range) 719 | if dp <= 0: 720 | raise ValueError('Width of lo frequency range must be positive') 721 | if da <= 0: 722 | raise ValueError('Width of hi frequency range must be positive') 723 | 724 | # method check 725 | method2fun = {'plv': plv, 'mi_tort': mi_tort, 'mi_canolty': mi_canolty, 726 | 'ozkurt': ozkurt, 'glm': glm} 727 | pac_fun = method2fun.get(pac_method, None) 728 | if pac_fun == None: 729 | raise ValueError('PAC method given is invalid.') 730 | 731 | # Filter setup 732 | if filterfn is None: 733 | filterfn = firf 734 | if filter_kwargs is None: 735 | filter_kwargs = {'rmvedge':False} 736 | else: 737 | if filter_kwargs is None: 738 | filter_kwargs = {} 739 | 740 | 741 | # Calculate palette frequency parameters 742 | f_phases = np.arange(p_range[0], p_range[1], dp) 743 | f_amps = np.arange(a_range[0], a_range[1], da) 744 | P = len(f_phases) 745 | A = len(f_amps) 746 | 747 | # Calculate all phase time series 748 | phaseT = np.zeros(P,dtype=object) 749 | filterlens = np.zeros(P,dtype=int) 750 | for p in range(P): 751 | f_lo = (f_phases[p], f_phases[p] + dp) 752 | loF = filterfn(lo, f_lo, fs, w=w_lo, **filter_kwargs) 753 | phaseT[p] = np.angle(hilbert(loF)) 754 | filterlens[p] = np.int(w_lo*fs/np.float(f_lo[0])) 755 | if filterlens[p] >= len(lo)/2.: 756 | raise ValueError('The input signal is too short to estimate PAC without edge artifacts') 757 | 758 | # Calculate all amplitude time series 759 | ampT = np.zeros(A,dtype=object) 760 | for a in range(A): 761 | f_hi = (f_amps[a], f_amps[a] + da) 762 | hiF = filterfn(hi, f_hi, fs, w=w_hi, **filter_kwargs) 763 | ampT[a] = np.abs(hilbert(hiF)) 764 | if pac_method == 'plv': 765 | ampT[a] = filterfn(ampT[a], f_lo, fs, w=w_lo, **filter_kwargs) 766 | ampT[a] = np.angle(hilbert(ampT[a])) 767 | 768 | # Calculate PAC for every combination of P and A 769 | comod = np.zeros((P, A)) 770 | for p in range(P): 771 | for a in range(A): 772 | comod[p, a] = pac_fun(phaseT[p][filterlens[p]:-filterlens[p]], ampT[a][filterlens[p]:-filterlens[p]], [], [], fs=fs, filterfn=False) 773 | return comod 774 | 775 | 776 | def pa_series(lo, hi, f_lo, f_hi, fs=1000, w_lo=3, w_hi=3, 777 | filterfn=None, filter_kwargs=None, hi_phase=False): 778 | """ 779 | Calculate the phase and amplitude time series 780 | 781 | Parameters 782 | ---------- 783 | lo : array-like, 1d 784 | The low frequency time-series to use as the phase component 785 | hi : array-like, 1d 786 | The high frequency time-series to use as the amplitude component 787 | f_lo : (low, high), Hz 788 | The low frequency filtering range 789 | f_hi : (low, high), Hz 790 | The low frequency filtering range 791 | w_lo : float 792 | Number of cycles for the filter order of the low band-pass filter 793 | w_hi : float 794 | Number of cycles for the filter order of the low band-pass filter 795 | fs : float 796 | The sampling rate (default = 1000Hz) 797 | filterfn : function 798 | The filtering function, `filterfn(x, f_range, filter_kwargs)` 799 | filter_kwargs : dict 800 | Keyword parameters to pass to `filterfn(.)` 801 | hi_phase : boolean 802 | Whether to calculate phase of low-frequency component of the high frequency 803 | time-series amplitude instead of amplitude of the high frequency time-series 804 | (default = False) 805 | 806 | Returns 807 | ------- 808 | pha : array-like, 1d 809 | Time series of phase 810 | amp : array-like, 1d 811 | Time series of amplitude (or phase of low frequency component of amplitude if hi_phase=True) 812 | 813 | Usage 814 | ----- 815 | >>> import numpy as np 816 | >>> from scipy.signal import hilbert 817 | >>> from pacpy.pac import pa_series 818 | >>> t = np.arange(0, 10, .001) # Define time array 819 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 820 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 821 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 822 | >>> pha, amp = pa_series(lo, hi, (4,8), (80,150)) 823 | >>> print pha 824 | [ 1.57079633 1.60849544 1.64619455 ..., 1.45769899 1.4953981 1.53309721] 825 | """ 826 | 827 | # Arg check 828 | _x_sanity(lo, hi) 829 | 830 | # Filter setup 831 | if filterfn is None: 832 | filterfn = firf 833 | 834 | if filter_kwargs is None: 835 | filter_kwargs = {} 836 | 837 | # Filter then hilbert 838 | if filterfn is not False: 839 | _range_sanity(f_lo, f_hi) 840 | lo = filterfn(lo, f_lo, fs, w=w_lo, **filter_kwargs) 841 | hi = filterfn(hi, f_hi, fs, w=w_hi, **filter_kwargs) 842 | 843 | lo = np.angle(hilbert(lo)) 844 | hi = np.abs(hilbert(hi)) 845 | 846 | # if high frequency should be returned as phase of low-frequency 847 | # component of the amplitude: 848 | if hi_phase == True: 849 | hi = filterfn(hi, f_lo, fs, w=w_lo, **filter_kwargs) 850 | hi = np.angle(hilbert(hi)) 851 | 852 | # Make arrays the same size 853 | lo, hi = _trim_edges(lo, hi) 854 | 855 | return lo, hi 856 | 857 | 858 | def pa_dist(pha, amp, Nbins=10): 859 | """ 860 | Calculate distribution of amplitude over a cycle of phases 861 | 862 | Parameters 863 | ---------- 864 | pha : array 865 | Phase time series 866 | amp : array 867 | Amplitude time series 868 | Nbins : int 869 | Number of phase bins in the distribution, 870 | uniformly distributed between -pi and pi. 871 | 872 | Returns 873 | ------- 874 | dist : array 875 | Average amplitude in each phase bins 876 | phase_bins : array 877 | The boundaries to each phase bin. Note the length is 1 + len(dist) 878 | 879 | Usage 880 | ----- 881 | >>> import numpy as np 882 | >>> from scipy.signal import hilbert 883 | >>> from pacpy.pac import pa_series, pa_dist 884 | >>> t = np.arange(0, 10, .001) # Define time array 885 | >>> lo = np.sin(t * 2 * np.pi * 6) # Create low frequency carrier 886 | >>> hi = np.sin(t * 2 * np.pi * 100) # Create modulated oscillation 887 | >>> hi[np.angle(hilbert(lo)) > -np.pi*.5] = 0 # Clip to 1/4 of cycle 888 | >>> pha, amp = pa_series(lo, hi, (4,8), (80,150)) 889 | >>> phase_bins, dist = pa_dist(pha, amp) 890 | >>> print dist 891 | [ 7.21154110e-01 8.04347122e-01 4.49207087e-01 2.08747058e-02 892 | 8.03854240e-05 3.45166617e-05 3.45607343e-05 3.51091029e-05 893 | 7.73644631e-04 1.63514941e-01] 894 | """ 895 | if np.logical_or(Nbins < 2, Nbins != int(Nbins)): 896 | raise ValueError( 897 | 'Number of bins in the low frequency oscillation cycle must be an integer >1.') 898 | if len(pha) != len(amp): 899 | raise ValueError( 900 | 'Phase and amplitude time series must be of same length.') 901 | 902 | phase_bins = np.linspace(-np.pi, np.pi, int(Nbins + 1)) 903 | dist = np.zeros(int(Nbins)) 904 | 905 | for b in range(int(Nbins)): 906 | t_phase = np.logical_and(pha >= phase_bins[b], 907 | pha < phase_bins[b + 1]) 908 | dist[b] = np.mean(amp[t_phase]) 909 | 910 | return phase_bins[:-1], dist 911 | --------------------------------------------------------------------------------