Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature WPLI #411

Merged
merged 70 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
fcf6c8f
created Weighted_Phase_Lag_Index-function (loop-approach) with corres…
ojoenlanuca Jan 27, 2021
a9bf961
simplified loop-approach for WPLI
ojoenlanuca Jan 27, 2021
e4d440f
reduced in-loop calculations
ojoenlanuca Jan 27, 2021
9325383
added as comment ARRAY-approach for WPLI
ojoenlanuca Jan 27, 2021
8ccacd3
added *.csv and *.mat files to be ignored for testing the WPLI-function
ojoenlanuca Jan 28, 2021
1148cbf
added ground-truth test for WPLI based on the LFP-dataset from ICN le…
ojoenlanuca Jan 28, 2021
f5e1d53
undo loop-'optimization', because it was wrong
ojoenlanuca Jan 29, 2021
f91ad9a
add test-function to compare loop&array-approach for wpli;; removed u…
ojoenlanuca Feb 3, 2021
470ff68
removed: 1) loop-approach, 2) compairson-test of loop&array-approach …
ojoenlanuca Feb 8, 2021
ab5c587
use list comprehension for data assembling
ojoenlanuca Feb 8, 2021
1568aea
added boolean-parameter 'abs' for optional direction information of t…
ojoenlanuca Feb 16, 2021
55c4f7e
added ground-truth test with simple artificial LFP-data
ojoenlanuca Feb 16, 2021
7cb448a
added documentation for absolute_value-parameter;; removed negative f…
ojoenlanuca Feb 22, 2021
ba0bf82
removed self-prefix from variables in setUp(), which are not used in …
ojoenlanuca Feb 22, 2021
d0f082b
updated imports;; simplified artificial-LFP data creation;; renamed v…
ojoenlanuca Feb 26, 2021
980d3b3
added new test-function, to cover parameter absolute_value set to Fal…
ojoenlanuca Feb 26, 2021
734ca66
rearanged parameter-order in the setUp of WPLI-TestCase to enhance re…
ojoenlanuca Feb 26, 2021
7e2396a
updated documentation of parameter 'absolute_value';; use now numpy.r…
ojoenlanuca Mar 1, 2021
ff3fa00
added generation-function for artificial LFP-datasets, which will be …
ojoenlanuca Mar 8, 2021
46dcac2
fixed bug: steps between frequencies were wrong due to wrong size par…
ojoenlanuca Mar 11, 2021
dd25a9b
migarated generation of artifical LFP datasets into a separate functi…
ojoenlanuca Mar 11, 2021
0544cab
added new test-function, to test if WPLI is close to the values from …
ojoenlanuca Mar 11, 2021
ebd4bf8
added error messages, to facilitate debugging, because several assert…
ojoenlanuca Mar 11, 2021
b2a9e37
setted equal_nan=True and remove not-nan-mask
ojoenlanuca Mar 11, 2021
4bd480d
updated comments & documentation;; updated paths and filenames of the…
ojoenlanuca Mar 16, 2021
38b1f96
updated line-length & visual indents to meet PEP8
ojoenlanuca Mar 17, 2021
b277cde
added wpli-function to __all__-list, so that the corresponding html d…
ojoenlanuca Mar 29, 2021
592b119
moved generation-function of artificial datasets for ground-truth-tes…
ojoenlanuca Mar 29, 2021
cdd3710
created/moved generation function of artificial datatsets for ground-…
ojoenlanuca Mar 29, 2021
b8dc100
updated error-message, to treat different shape cases (e.g. trial num…
ojoenlanuca Mar 29, 2021
ef95169
removed unnecessary else-case
ojoenlanuca Mar 29, 2021
91e1c58
joined tests for error-raising, while shape-checking of input-signals
ojoenlanuca Mar 29, 2021
8c06732
updated all tests, to cover the allowed input-types 'np.array' and 'Q…
ojoenlanuca Mar 30, 2021
1a47afc
updated wpli-function and all corresponding tests, to support now als…
ojoenlanuca Mar 30, 2021
6b0f2e3
use now python unittest.subTest for testing the different input-types…
ojoenlanuca Mar 31, 2021
6176fbd
fixed: too long lines
ojoenlanuca Mar 31, 2021
283fc7f
updated input-check: compare input-type explicitly with neo.AnalogSignal
ojoenlanuca Apr 12, 2021
39785fe
updated input-check: now raise ValueError for: unequal sampling_rates…
ojoenlanuca Apr 12, 2021
131f5ab
enhance readability: iterate now over configuration-dict containing t…
ojoenlanuca Apr 12, 2021
229a2ba
replaced real LFP-dataset from ICN-tutorial with cutting from R2G-dat…
ojoenlanuca Apr 13, 2021
6b8dfd3
added two error-raising testfunctions;; simplified rescale-operation …
ojoenlanuca Apr 19, 2021
2cc71f8
updated(lowered) tolerance for ground-truth consistency with real LFP…
Apr 27, 2021
2a463f0
removed plot-function
Apr 27, 2021
73e2821
set random-seed;; save also cross-spectrum, to prevent having differe…
Apr 27, 2021
3cb55f3
corrected FieldTrip function-name
May 3, 2021
af83cc6
removed plot-function;; added generic-path to FieldTrip-toolbox
May 3, 2021
a6f1447
removed plot-function and self-made realfft-function;; added generic …
May 3, 2021
9e6af64
corrected file-naming for cross-spectrum of artificial-data
May 3, 2021
22e3eec
use now cross-spectrum calculated with numpy.fft as argument for ft_c…
May 3, 2021
3335535
removed multitaper-comparision test-function, because different fft-t…
May 3, 2021
5afd15d
corrected spelling errors & grammer;; added docstrings to error-raisi…
ojoenlanuca May 3, 2021
46835aa
updated comments & removed debug-statements in MatLab-script
ojoenlanuca May 3, 2021
d45ae1c
use pathlib.Path instead of os.path now;; optimized imports;; added d…
ojoenlanuca May 11, 2021
25c3ac2
mne_spectral_connectivity and ft_connectivity -Scripts are saved loca…
ojoenlanuca Jun 21, 2021
798eb77
use now elephant.test.download to download the needed files from the …
ojoenlanuca Jul 21, 2021
43d5b90
use standard string for URLs, instead of pathlib.Path, which should b…
ojoenlanuca Jul 21, 2021
f5484b6
clarify docstring about directionality info of wpli
ojoenlanuca Sep 8, 2021
1c40e3a
updated docstrings of ground truth consistency test functions to be m…
ojoenlanuca Sep 10, 2021
78a9097
removed cutout from ch03 of files_to_download, because not needed; re…
ojoenlanuca Sep 10, 2021
0ab866a
detailed the data-types of the parameters and return values
ojoenlanuca Sep 10, 2021
fcb54ae
Merge branch 'master' into feature_WPLI
Jul 12, 2022
a3d01b5
add weighted_phase_lag_index to autosummary
Jul 12, 2022
09f0596
update citations and add references to elephant.bib
Jul 12, 2022
b4ef72a
update tests with download_datasets from datasets
Jul 12, 2022
0c7db1e
remove unnecessary commas
Jul 12, 2022
cb3c60b
new structure of elephant-data applied to unit-test data on GIN, link…
Jul 13, 2022
d4c36a9
fixed deprecated name, np.complex
Nov 14, 2022
ec4ebd4
fixed pep8, fixed formula
Nov 14, 2022
9962612
merge master
Feb 23, 2023
c7bd4df
Merge branch 'master' into feature_WPLI
Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ ignored/

# data
*.nix
*.csv
*.mat

# neo logs
**/logs
92 changes: 91 additions & 1 deletion elephant/phase_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"spike_triggered_phase",
"phase_locking_value",
"mean_phase_vector",
"phase_difference"
"phase_difference",
"weighted_phase_lag_index"
]


Expand Down Expand Up @@ -307,3 +308,92 @@ def phase_difference(alpha, beta):
delta = alpha - beta
phase_diff = np.arctan2(np.sin(delta), np.cos(delta))
return phase_diff


def weighted_phase_lag_index(signal_i, signal_j, sampling_frequency=None,
absolute_value=True):
r"""
Calculates the Weigthed Phase-Lag Index (WPLI)

This function estimates the WPLI, which is a measure of phase-synchrony. It
describes for two given signals i and j, who is leading/lagging the other
signal in the frequency domain across multiple trials.

Parameters
----------
signal_i, signal_j : np.array, Quantity, neo.AnalogSignal
Time-series of the first and second signals,
with `t` time points and `n` trials.
sampling_frequency : quantity (default: None)
Sampling frequency of the signals in Hz. Not needed if signal i and j
are neo.AnalogSignals.
absolute_value : boolean (default: True)
Takes the absolute value of the numerator in the WPLI-formula.
When set to `False`, the WPLI contains additional directionality
information about which signal leads/lags the other signal:
- wpli > 0 : first signal leads second one
- wpli < 0 : first signal lags second one

Returns
Moritz-Alexander-Kern marked this conversation as resolved.
Show resolved Hide resolved
-------
freqs : quantity
Positive frequencies in Hz associated with the estimates of `wpli`.
Range: :math:`[0, sampling frequency/2]`
wpli : float
Weighted phase-lag index of `signal_i` and `signal_j` across trials.
Range: :math:`[0, 1]`

Raises
------
ValueError
If trial number or trial length are different for signal i and j.

Notes
-----
This implementation is based on the formula taken from [1] (pp.1550) :

.. math::
WPLI = \frac{| E( |Im(X)| * sgn(Im(X)) ) |}{E( |Im(X)| )}

with:
- E{...} : expected value operator
- Im{X} : imaginary component of the cross-spectrum
- X = Z_i * Z_j_conjugate : as cross-spectrum
- Z_i, Z_j : complex-valued matrix, representing the Fourier spectra
of a particular frequency of the signals i and j.

References
----------
.. [1] Martin Vinck, Robert Oostenveld, Marijn van Wingerden,
Franscesco Battaglia, Cyriel M.A. Pennartz; "An improved index of
phase-synchronization for electrophysiological data in the presence
of volume-conduction, noise and sample-size bias"; NeuroImage, vol. 55,
pp. 1548-1565, 2011

"""
if sampling_frequency is None: # neo.AnalogSignal-input
Moritz-Alexander-Kern marked this conversation as resolved.
Show resolved Hide resolved
sampling_frequency = signal_i.sampling_rate
signal_i = signal_i.magnitude
signal_j = signal_j.magnitude

if np.shape(signal_i) != np.shape(signal_j):
if len(signal_i) != len(signal_j):
raise ValueError("trial number of signal i and j must be equal")
raise ValueError("trial length of signal i and j must be equal")

# ARRAY-approach
# calculate Fourier transforms
fft1 = np.fft.rfft(signal_i)
fft2 = np.fft.rfft(signal_j)
freqs = np.fft.rfftfreq(np.shape(signal_i)[1], d=1.0 / sampling_frequency)

# obtain cross-spectrum
cs = fft1 * np.conjugate(fft2)
# calculate WPLI
wpli_num = np.mean(np.abs(np.imag(cs)) * np.sign(np.imag(cs)), axis=0)
if absolute_value:
wpli_num = np.abs(wpli_num)
wpli_den = np.mean(np.abs(np.imag(cs)), axis=0)
wpli = wpli_num / wpli_den

return freqs, wpli
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import os

import numpy as np
from scipy.io import savemat


def _generate_datasets_for_ground_truth_testing(ntrial=40, tlength=2.5,
srate=250):
"""
Generates simple sinusoidal, artificial LFP-datasets.

This function simulates the recording of two LFP-signals over several
trials, for a certain duration of time and with a specified sampling
rate. These datasets will be used to calculate WPLI-ground-truth
with the MATlAB package FieldTrip and its function
ft_connectivity_wpli(), its wrapper ft_connectivityanalysis() and
with MNEs spectral_connectivity(). The last two use both multitaper
for FFT, therefore just certain frequencies will be compared.

Parameters
----------
ntrial: int
Number of trials in the datasets.
tlength: float
Time length of the datasets in seconds.
srate: float
Sampling rate used to 'record' the signals in Hz.

Returns
-------
None

Notes
-----
Used versions of MATLAB & FieldTrip:
MATLAB Version 9.9 (2020b)
FieldTrip fieldtrip-20210128

Instead of using the ft_connectivityanalysis() wrapper function, which
expects preprocessed data, the ft_connectivity_wpli() is called
directly. That's because the preprocessing functions of FieldTrip have
no 'conventional' Fourier-Transformation (FFT) method, but among
others a multitaper-FFT. Because this python-version of WPLI doesn't
use multitaper, but conventional FFT, the utilized MATLAB script also
uses the conventional FFT to preprocess the data and calculate the
cross-spectrum, which will then be passed to the ft_connectivity_wpli.

"""
times = np.arange(0, tlength, 1 / srate)

# lfps_1 & 2 will be used for
# 1) calculating ground-truth with FieldTrips' ft_connectivity_wpli
# 2) comparison to mutlitaper-approaches like
# FieldTrips' ft_connectivityanalysis()
# and MNEs' spectral_connectivity at certain frequencies
lfps_1 = [np.cos(2 * 16 * np.pi * times + np.pi / 2) +
np.cos(2 * 36 * np.pi * times + np.pi / 2) +
np.cos(2 * 52 * np.pi * times) +
np.cos(2 * 100 * np.pi * times) +
np.cos(2 * 70 * np.pi * times + np.pi / 2 + np.pi * (i % 2)) +
np.random.normal(loc=0.0, scale=1, size=len(times))
for i in range(ntrial)]
lfps_1 = np.stack(lfps_1, axis=0)

lfps_2 = [np.cos(2 * 16 * np.pi * times) +
np.cos(2 * 36 * np.pi * times) +
np.cos(2 * 52 * np.pi * times + np.pi / 2) +
np.cos(2 * 100 * np.pi * times + np.pi / 2) +
np.cos(2 * 70 * np.pi * times) +
np.random.normal(loc=0.0, scale=1, size=len(times))
for _ in range(ntrial)]
lfps_2 = np.stack(lfps_2, axis=0)

# save artificial LFP-dataset to .mat files
mdic_1 = {"lfp_matrix": lfps_1, "time": times, "sf": srate}
mdic_2 = {"lfp_matrix": lfps_2, "time": times, "sf": srate}
filename1_artificial = os.path.sep.join(['cross_testing_scripts',
'artificial_LFPs_1.mat'])
filename2_artificial = os.path.sep.join(['cross_testing_scripts',
'artificial_LFPs_2.mat'])

savemat(filename1_artificial, mdic_1)
savemat(filename2_artificial, mdic_2)


def main():
"""Run generation function."""
_generate_datasets_for_ground_truth_testing()


if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
%% use FieldTrips' ft_connectivity_wpli() to calculate WPLI ground-truth
%% BEFORE using this script: change current folder to cross_testing_scripts

% ARTIFICIAL DATA more complex
dataset1 = load('artificial_LFPs_1.mat');
dataset2 = load('artificial_LFPs_2.mat');

lfp1 = dataset1.lfp_matrix;
lfp2 = dataset2.lfp_matrix;
Fs = double(dataset1.sf);

siz = size(lfp1)
trial = siz(1)
tlength = siz(2)

fft1 = realfft(lfp1, tlength, 2);
fft2 = realfft(lfp2, tlength, 2);
freq = 0:double(Fs/tlength):(Fs/2); %-0.1 to stay lower Fs/2

cs = fft1 .* conj(fft2);
length_fft = floor(tlength/2) +1
cs = reshape(cs, [trial, 1, length_fft]);

[wpli_1, v, n] = ft_connectivity_wpli(cs, 'feedback', 'text', 'dojack', 0, 'debias', 0);
% wpli_1 = abs(wpli_1); %(optional)

writematrix(wpli_1, 'ground_truth_WPLI_from_ft_connectivity_wpli_with_artificial_LFPs.csv');


figure(1);
plot(freq, wpli_1);

function realfft = realfft(a, tlength, dim)
fft_all = fft(a, tlength, dim);
realfft = fft_all(:, 1:(floor(length(fft_all)/2)+1));
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
%% use FieldTrips' ft_connectivityanalysis() to calculate WPLI ground-truth
%% BEFORE using this script: change current folder to cross_testing_scripts

% ARTIFICIAL DATA
dataset1 = load('artificial_LFPs_1.mat');
dataset2 = load('artificial_LFPs_2.mat');

siz = size(dataset1.lfp_matrix);
ntrials = siz(1);
nsamples = siz(2);

% create a data-structure, which is compatible with FieldTrip
joined_data.label = {'sig_1', 'sig_2'};
joined_data.trial = cell(1, ntrials);
for t = 1:ntrials
joined_data.trial(1, t) = {[dataset1.lfp_matrix(t, :); dataset2.lfp_matrix(t, :)]};
end
joined_data.time = cell(1, ntrials);
for t = 1:ntrials
joined_data.time(1, t) = {dataset1.time};
end
joined_data.fsample = double(dataset1.sf);

% do frequency-analysis
cfg_freq = [];
cfg_freq.output = 'fourier';
% mtmfft
cfg_freq.method = 'mtmfft';
cfg_freq.keeptrials = 'yes';
cfg_freq.tapsmofrq = 0.4;
cfg_freq.taper = 'dpss';
cfg_freq.polyremoval= -1;

freqfourier = ft_freqanalysis(cfg_freq, joined_data);

% do WPLI-calculation
cfg_wpli = [];
cfg_wpli.method = 'wpli';
ft_wpli = ft_connectivityanalysis(cfg_wpli, freqfourier);

% take absolute value (optional)
wpli_2 = ft_wpli.wplispctrm(1, 2, 1:(end-1));
wpli_2 = reshape(wpli_2, [1, length(wpli_2)]);
% wpli_2 = abs(wpli_2);

writematrix(wpli_2, 'ground_truth_WPLI_from_ft_connectivityanalysis_with_artificial_LFPs_multitaped.csv');


figure(2);
plot(ft_wpli.freq(1:(end-1)), wpli_2);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os

import matplotlib.pyplot as plt
import numpy as np
import quantities as pq
import scipy
from mne.connectivity import spectral_connectivity
from scipy.io import savemat


def main():
"""
This script calculates a WPLI ground-truth with
MNE's spectral_connectivity(), which uses multitaper for FFT, so that
just certain frequencies will be compared to the results of
weighted_phase_lag_index(), which uses conventional FFT.
"""
# Load first & second data file
filename1 = os.path.sep.join(['artificial_LFPs_1.mat'])
dataset1 = scipy.io.loadmat(filename1, squeeze_me=True)

filename2 = os.path.sep.join(['artificial_LFPs_2.mat'])
dataset2 = scipy.io.loadmat(filename2, squeeze_me=True)

# get the relevant values
lfps1 = dataset1['lfp_matrix'] * pq.uV
sf1 = dataset1['sf'] * pq.Hz
lfps2 = dataset2['lfp_matrix'] * pq.uV

# data-shape: epochs/trial X signals X times
lfp_both_signals = np.hstack((lfps1.magnitude, lfps2.magnitude))
lfp_both_signals = lfp_both_signals.reshape(np.shape(lfps1)[0], 2,
np.shape(lfps1)[1])

wpli, freqs, times, n_epochs, n_tapers = spectral_connectivity(
data=lfp_both_signals, method='wpli', sfreq=sf1.magnitude,
verbose=True, mode='fourier', fmin=0)

# get relevant relation: first signal compared to the second one
wpli = wpli[1][0][0:]
freqs = freqs[0:]

fig, ax = plt.subplots(1, 1, figsize=(10, 8), num=1)
fig.suptitle("Weighted Phase Lag Index - Ground Truth from MNE", size=20)
ax.plot(freqs, wpli, label="WPLI")
ax.set_xlabel('f (Hz)', size=16)
ax.legend(fontsize=16, framealpha=0)
plt.show()

np.savetxt("ground_truth_WPLI_from_MNE_spectral_connectivity"
"_with_artificial_LFPs_multitaped.csv",
wpli, delimiter=",", fmt="%20.20e")


if __name__ == '__main__':
main()
Loading