Phase Locking
Calculating Phase locking with bmtool¶
By Gregory Glickert
RunningInCOLAB = 'google.colab' in str(get_ipython())
if RunningInCOLAB:
%pip install bmtool &> /dev/null
import numpy as np
from scipy import signal as ss
import matplotlib.pyplot as plt
from bmtool.analysis.lfp import fit_fooof, calculate_spike_lfp_plv, calculate_ppc, calculate_ppc2
np.random.seed(9) # lucky number 9
fs = 1000 # 1 kHz sampling rate
duration = 200 # 200 seconds
t = np.arange(0, duration, 1/fs)
# Define oscillation frequencies
beta_freq = 15 # Hz
gamma_freq = 40 # Hz
# Create a simulated LFP with multiple frequency components
lfp = (0.5 * np.sin(2 * np.pi * gamma_freq * t) + 0.2 * np.sin(2 * np.pi * beta_freq * t) + 0.3 * np.random.randn(len(t)))
# Generate phase information for each frequency
beta_phase = np.angle(ss.hilbert(np.sin(2 * np.pi * beta_freq * t)))
gamma_phase = np.angle(ss.hilbert(np.sin(2 * np.pi * gamma_freq * t)))
# Generate beta-synchronized spikes
# Strong preference for specific beta phase, weak response to gamma
beta_spike_probability = (0.8 * (1 + np.cos(beta_phase - np.pi/4)) +
0.1 * (1 + np.cos(gamma_phase - np.pi/3)))
beta_spike_train = np.random.rand(len(t)) < beta_spike_probability * 0.02
beta_spike_times = t[beta_spike_train]
# Generate gamma-synchronized spikes
# Strong preference for specific gamma phase, weak response to beta
gamma_spike_probability = (0.1 * (1 + np.cos(beta_phase - np.pi/4)) +
0.8 * (1 + np.cos(gamma_phase - np.pi/3)))
gamma_spike_train = np.random.rand(len(t)) < gamma_spike_probability * 0.02
gamma_spike_times = t[gamma_spike_train]
print(f"Generated {len(beta_spike_times)} beta-synchronized spikes")
print(f"Generated {len(gamma_spike_times)} gamma-synchronized spikes")
Generated 359175 beta-synchronized spikes Generated 361180 gamma-synchronized spikes
We can see that we generated an LFP with two different frequencies¶
hz,pxx = ss.welch(lfp,fs)
_,_ = fit_fooof(hz,pxx,plot=True,report=True,plt_range=(1,100),freq_range=[1,100])
================================================================================================== FOOOF - POWER SPECTRUM MODEL The model was run on the frequency range 3 - 98 Hz Frequency Resolution is 3.91 Hz Aperiodic Parameters (offset, exponent): -3.8963, -0.0772 2 peaks were found: CF: 14.99, PW: 1.233, BW: 7.81 CF: 39.84, PW: 2.125, BW: 7.81 Goodness of fit metrics: R^2 of model fit is 0.9811 Error of the fit is 0.0531 ==================================================================================================
Now we can see how our spike times are entrained to the lfp we made. There are a few different metrics that we can use.¶
1. Phase-Locking Value (PLV) - calculate_spike_lfp_plv
¶
Equation: $$ \text{PLV} = \frac{\left|\sum_{j=1}^{n} e^{i\phi_j}\right|^2}{n^2} $$
Where:
- $\phi_j$ = Phase of LFP at spike time $j$
- $n$ = Number of spikes
- $i$ = Imaginary unit
This measures phase concentration by calculating the squared magnitude of the normalized resultant vector of spike-phase angles.
2. Pairwise Phase Consistency (PPC) - calculate_ppc
¶
Equation: $$ \text{PPC} = \frac{2}{n(n-1)} \sum_{j=1}^{n-1} \sum_{k=j+1}^{n} \cos(\phi_j - \phi_k) $$
Where:
- $\phi_j,\phi_k$ = Phases at spike times $j$ and $k$
- $n$ = Number of spikes
This calculates phase consistency through pairwise comparisons of spike-phase differences.
3. Optimized Pairwise Phase Consistency (PPC2) - calculate_ppc2
¶
Derivation of PPC2 from PPC¶
1. Original PPC¶
$$ \text{PPC} = \frac{2}{n(n-1)} \sum_{i < j} \cos(\phi_i - \phi_j) $$
2. Rewrite $\cos(\phi_i - \phi_j)$ Using Euler’s Formula¶
$$ \cos(\phi_i - \phi_j) = \text{Re}\left( e^{i(\phi_i - \phi_j)} \right) $$
3. Sum Over All Pairs¶
$$ \sum_{i < j} \cos(\phi_i - \phi_j) = \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \cos(\phi_i - \phi_j) - \frac{n}{2} $$
4. Express in Terms of Complex Exponentials¶
$$ \sum_{i < j} \cos(\phi_i - \phi_j) = \frac{1}{2} \text{Re} \left( \left| \sum_{j=1}^n e^{i\phi_j} \right|^2 \right) - \frac{n}{2} $$
5. Final PPC2 Formula¶
$$ \text{PPC2} = \frac{\left|\sum_{j=1}^{n} e^{i\phi_j}\right|^2 - n}{n(n-1)} $$
This algebraic equivalent of PPC provides computational optimization by:
- Converting phases to complex unit vectors
- Using vector magnitude properties to avoid explicit pairwise comparisons
All three metrics quantify phase locking between spike times and LFP oscillations
- all metrics should be the same with enough spike times
# spikes were generated on second time scale so spike_fs=1
print(calculate_spike_lfp_plv(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=40))
# 3 different methods for ppc all give the same answer
print(calculate_ppc(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=40,ppc_method='numpy'))
print(calculate_ppc(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=40,ppc_method='numba'))
print(calculate_ppc(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=40,ppc_method='gpu'))
print(calculate_ppc2(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=40))
0.19538329962305528 0.19538329962305606 0.195383299623056
We can also check the entrainment at different frequencies¶
freqs = [1,5,15,25,30,40,50,60,70,80]
ppc_gamma = []
ppc_beta = []
for i in range(len(freqs)):
ppc_gamma.append(calculate_ppc2(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=freqs[i]))
ppc_beta.append(calculate_ppc2(spike_times=beta_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=freqs[i]))
plt.plot(freqs,ppc_gamma,'o-', linewidth=2,label="FSI")
plt.plot(freqs,ppc_beta,'o-', linewidth=2,label='LTS')
plt.legend()
plt.show()
--------------------------------------------------------------------------- KeyboardInterrupt Traceback (most recent call last) Cell In[14], line 6 3 ppc_beta = [] 5 for i in range(len(freqs)): ----> 6 ppc_gamma.append(calculate_ppc2(spike_times=gamma_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=freqs[i])) 7 ppc_beta.append(calculate_ppc2(spike_times=beta_spike_times, lfp_signal=lfp,spike_fs=1, lfp_fs=fs, method='wavelet',freq_of_interest=freqs[i])) 9 plt.plot(freqs,ppc_gamma,'o-', linewidth=2,label="FSI") File ~/cortex_modeling/bmtool/bmtool/analysis/lfp.py:612, in calculate_ppc2(spike_times, lfp_signal, spike_fs, lfp_fs, method, freq_of_interest, lowcut, highcut, bandwidth) 609 raise ValueError("freq_of_interest must be provided for the wavelet method.") 611 # Apply CWT to extract phase at the frequency of interest --> 612 lfp_complex = wavelet_filter(x=lfp_signal, freq=freq_of_interest, fs=lfp_fs, bandwidth=bandwidth) 613 instantaneous_phase = np.angle(lfp_complex) 615 elif method == 'hilbert': File ~/cortex_modeling/bmtool/bmtool/analysis/lfp.py:284, in wavelet_filter(x, freq, fs, bandwidth, axis) 282 wavelet = 'cmor' + str(2 * bandwidth ** 2) + '-1.0' 283 scale = pywt.scale2frequency(wavelet, 1) * fs / freq --> 284 x_a = pywt.cwt(x, [scale], wavelet=wavelet, axis=axis)[0][0] 285 return x_a File ~/miniconda3/envs/bmtk/lib/python3.8/site-packages/pywt/_cwt.py:158, in cwt(data, scales, wavelet, sampling_period, method, axis) 156 if method == 'conv': 157 if data.ndim == 1: --> 158 conv = np.convolve(data, int_psi_scale) 159 else: 160 # batch convolution via loop 161 conv_shape = list(data.shape) File <__array_function__ internals>:180, in convolve(*args, **kwargs) File ~/miniconda3/envs/bmtk/lib/python3.8/site-packages/numpy/core/numeric.py:844, in convolve(a, v, mode) 842 if len(v) == 0: 843 raise ValueError('v cannot be empty') --> 844 return multiarray.correlate(a, v[::-1], mode) KeyboardInterrupt: