Skip to content

Commit

Permalink
[MRG] Implement VAR models over time and epochs (#23)
Browse files Browse the repository at this point in the history
Implement vector_auto_regressive function to estimate a VAR model from Epochs of data.

Added an example showing the VAR function.

Added additional mixing functions for dynamical "connectivity" functions.
  • Loading branch information
adam2392 authored Aug 18, 2021
1 parent 5fc3607 commit 373797a
Show file tree
Hide file tree
Showing 27 changed files with 1,183 additions and 50 deletions.
29 changes: 25 additions & 4 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,24 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest ]
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
include:
- os: ubuntu-latest
python-version: 3.6
- os: ubuntu-latest
python-version: 3.7
- os: ubuntu-latest
python-version: 3.8
- os: macos-latest
python-version: 3.8

env:
TZ: Europe/Berlin
FORCE_COLOR: true
DISPLAY: ':99.0'
MNE_LOGGING_LEVEL: 'info'
OPENBLAS_NUM_THREADS: '1'
PYTHONUNBUFFERED: '1'

steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
Expand All @@ -116,6 +128,10 @@ jobs:
# path: ~/.cache/pip
# key: ${{ hashFiles('setup.py') }}-${{ hashFiles('test-requirements.txt') }}-${{ hashFiles('requirements.txt') }}

- run: ./tools/setup_xvfb.sh
name: 'Setup xvfb'
if: runner.os == 'Linux'

- name: Install dependencies
run: |
pip install --upgrade --upgrade-strategy eager -r requirements.txt
Expand All @@ -127,10 +143,15 @@ jobs:
date
python --version
which python
# python -c "import mne; mne.sys_info()"
- name: Install MNE-connectivity
run: pip install --no-deps .
run: |
pip install --no-deps .
- shell: bash -el {0}
run: mne sys_info
name: 'Show infos'

- name: Run pytest
run: |
python -m pytest . --cov=mne_connectivity --cov-report=xml --cov-config=setup.cfg --verbose --ignore mne-python
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ include requirements_doc.txt
recursive-include examples *.py
recursive-include examples *.txt

recursive-include mne *.py
recursive-include mne_connectivity *.py

### Exclude

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Contributing to MNE-Connectivity

Please see the documentation on the MNE-Connectivity homepage:

https://mne.tools/mne-connectivity/dev/install/contributing.html
https://github.com/mne-tools/mne-connectivity/blob/main/CONTRIBUTING.md


Forum
Expand Down
2 changes: 1 addition & 1 deletion doc/_templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{% endblock %}

{% block extrahead %}
<link rel="canonical" href="https://mne.tools/mne-bids/stable/index.html" />
<link rel="canonical" href="https://mne.tools/mne-connectivity/stable/index.html" />
<script type="text/javascript" src="{{ pathto('_static/copybutton.js', 1) }}"></script>
<script type="text/javascript" src="{{ pathto('_static/scrollfix.js', 1) }}"></script>
{{ super() }}
Expand Down
5 changes: 3 additions & 2 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ listed above.
envelope_correlation
phase_slope_index
spectral_connectivity
vector_auto_regression

Other functions
===============
Reading functions
=================

.. autosummary::
:toctree: generated/
Expand Down
5 changes: 3 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,19 @@
'navigation_with_keys': False,
'show_toc_level': 1,
}
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['search-field.html'],
}

html_context = {
'versions_dropdown': {
'dev': 'v0.1 (devel)',
'dev': 'v0.2 (devel)',
'v0.1': 'v0.1',
},
}

html_sidebars = {'**': ['localtoc.html']}
# html_sidebars = {'**': ['localtoc.html']}

# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
Expand Down
24 changes: 24 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
% Encoding: UTF-8
@article{Dawson_2016,
author={Dawson, Scott T. M. and Hemati, Maziar S. and Williams, Matthew O. and Rowley, Clarence W.},
DOI={10.1007/s00348-016-2127-7},
ISSN={1432-1114},
journal={Experiments in Fluids},
month={Feb},
number={3},
publisher={Springer Science and Business Media LLC},
title={Characterizing and correcting for the effect of sensor noise in the dynamic mode decomposition},
url={http://dx.doi.org/10.1007/s00348-016-2127-7},
volume={57},
year={2016},
}

@article{VinckEtAl2011,
author = {Vinck, Martin and Oostenveld, Robert and {van Wingerden}, Marijn and Battaglia, Franscesco and Pennartz, Cyriel M.A.},
Expand Down Expand Up @@ -100,3 +113,14 @@ @article{NolteEtAl2004
volume = {115},
year = {2004}
}

@INPROCEEDINGS{li_linear_2017,
author = {Li, Adam and Gunnarsdottir, Kristin M. and Inati, Sara and Zaghloul, Kareem and Gale, John and Bulacio, Juan and Martinez-Gonzalez, Jorge and Sarma, Sridevi V.},
booktitle = {2017 39th Annual International Conference of the IEEE Engineering in Medicine and Biology Society (EMBC)},
title = {Linear time-varying model characterizes invasive EEG signals generated from complex epileptic networks},
year = {2017},
volume = {},
number = {},
pages = {2802-2805},
doi = {10.1109/EMBC.2017.8037439}
}
1 change: 1 addition & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Changelog
- Adding `EpochConnectivity`, `EpochTemporalConnectivity`, `EpochSpectralConnectivity` and `EpochSpectroTemporalConnectivity` as a data structure to hold connectivity data over Epochs, by `Adam Li`_ (:gh:`6`)
- ``indices`` argument in Connectivity classes can now be ``symmetric``, allowing for memory-efficient storage of symmetric connectivity, by `Adam Li`_ (:gh:`20`)
- New function ``save`` in Connectivity classes along with :func:`read_connectivity` can now be used to write and read Connectivity data as netCDF files, by `Adam Li`_ (:gh:`20`)
- New function :func:`vector_auto_regression` to compute dynamic connectivity vector auto-regressive (VAR) model, by `Adam Li`_ (:gh:`23`)

Bug
~~~
Expand Down
7 changes: 7 additions & 0 deletions examples/dynamic/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Dynamic Connectivity Examples
-----------------------------

Examples demonstrating connectivity analysis with dynamics. For example,
this can be a vector auto-regressive model (also known as a linear
dynamical system). These classes of models are generative and
model the dynamics and evolution of the data.
212 changes: 212 additions & 0 deletions examples/dynamic/mne_var_connectivity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""
.. _ex-var:
===================================================
Compute vector autoregressive model (linear system)
===================================================
Compute a VAR (linear system) model from time-series
activity :footcite:`li_linear_2017` using a
continuous iEEG recording.
In this example, we will demonstrate how to compute
a VAR model with different statistical assumptions.
"""

# Authors: Adam Li <adam2392@gmail.com>
#
# License: BSD (3-clause)
import numpy as np

import mne
from mne import make_fixed_length_epochs
from mne_bids import BIDSPath, read_raw_bids

import matplotlib.pyplot as plt

from mne_connectivity import vector_auto_regression

# %%
# Load the data
# -------------
# Here, we first download an ECoG dataset that was recorded from a patient with
# epilepsy. To facilitate loading the data, we use `mne-bids
# <https://mne.tools/mne-bids/>`_.
#
# Then, we will do some basic filtering and preprocessing using MNE-Python.

# paths to mne datasets - sample ECoG
bids_root = mne.datasets.epilepsy_ecog.data_path()

# first define the BIDS path
bids_path = BIDSPath(root=bids_root, subject='pt1', session='presurgery',
task='ictal', datatype='ieeg', extension='vhdr')

# Then we'll use it to load in the sample dataset. Here we use a format (iEEG)
# that is only available in MNE-BIDS 0.7+, so it will emit a warning on
# versions <= 0.6
raw = read_raw_bids(bids_path=bids_path, verbose=False)

line_freq = raw.info['line_freq']
print(f'Data has a power line frequency at {line_freq}.')

# Pick only the ECoG channels, removing the ECG channels
raw.pick_types(ecog=True)

# Load the data
raw.load_data()

# Then we remove line frequency interference
raw.notch_filter(line_freq)

# drop bad channels
raw.drop_channels(raw.info['bads'])

# %%
# Crop the data for this example
# ------------------------------
#
# We find the onset time of the seizure and remove all data after that time.
# In this example, we are only interested in analyzing the interictal
# (non-seizure) data period.
#
# One might be interested in analyzing the seizure period also, which we
# leave as an exercise for our readers!

# Find the annotated events
events, event_id = mne.events_from_annotations(raw)

# get sample at which seizure starts
onset_id = event_id['onset']
onset_idx = np.argwhere(events[:, 2] == onset_id)
onset_sample = events[onset_idx, 0].squeeze()
onset_sec = onset_sample / raw.info['sfreq']

# remove all data after the seizure onset
raw = raw.crop(tmin=0, tmax=onset_sec, include_tmax=False)

# %%
# Create Windows of Data (Epochs) Using MNE-Python
# ------------------------------------------------
# We have a continuous iEEG snapshot that is about 60 seconds long
# (after cropping). We would like to estimate a VAR model over a sliding window
# of 500 milliseconds with a 250 millisecond step size.
#
# We can use `mne.make_fixed_length_epochs` to create an Epochs data structure
# representing this sliding window.

epochs = make_fixed_length_epochs(raw=raw, duration=0.5, overlap=0.25)
times = epochs.times
ch_names = epochs.ch_names

print(epochs)
print(epochs.times)
print(epochs.event_id)
print(epochs.events)


# %%
# Compute the VAR model for all windows
# -------------------------------------
# Now, we are ready to compute our VAR model. We will compute a VAR model for
# each Epoch and return an EpochConnectivity data structure. Each Epoch here
# represents a separate VAR model. Taken together, these represent a
# time-varying linear system.

conn = vector_auto_regression(
data=epochs.get_data(), times=times, names=ch_names)

# this returns a connectivity structure over time
print(conn)

# %%
# Evaluate the VAR model fit
# ---------------------------
# We can now evaluate the model fit by computing the residuals of the model and
# visualizing them. In addition, we can evaluate the covariance of the
# residuals. This will compute an independent VAR model for each epoch (window)
# of data.

predicted_data = conn.predict(epochs.get_data())

# compute residuals
residuals = epochs.get_data() - predicted_data

# visualize the residuals
fig, ax = plt.subplots()
ax.plot(residuals.flatten(), '*')
ax.set(
title='Residuals of fitted VAR model',
ylabel='Magnitude'
)

# compute the covariance of the residuals
model_order = conn.attrs.get('model_order')
t = residuals.shape[0]
sampled_residuals = np.concatenate(
np.split(residuals[:, :, model_order:], t, 0),
axis=2
).squeeze(0)
rescov = np.cov(sampled_residuals)

# Next, we visualize the covariance of residuals.
# Here we will see that because we use ordinary
# least-squares as an estimation method, the residuals
# should come with low covariances.
fig, ax = plt.subplots()
cax = fig.add_axes([0.27, 0.8, 0.5, 0.05])
im = ax.imshow(rescov, cmap='viridis', aspect='equal', interpolation='none')
fig.colorbar(im, cax=cax, orientation='horizontal')

# %%
# Compute one VAR model using all epochs
# --------------------------------------
# By setting ``model='dynamic'``, we instead treat each Epoch as a sample of
# the same VAR model and thus we only estimate one VAR model. One might do this
# when one suspects the data is stationary and one VAR model represents all
# epochs.

conn = vector_auto_regression(
data=epochs.get_data(), times=times, names=ch_names,
model='avg-epochs')

# this returns a connectivity structure over time
print(conn)

# %%
# Evaluate model fit again
# ------------------------
# We can now evaluate the model fit again as done earlier. This model fit will
# of course have higher residuals than before as we are only fitting 1 VAR
# model to all the epochs.

first_epoch = epochs.get_data()[0, ...]
predicted_data = conn.predict(first_epoch)

# compute residuals
residuals = epochs.get_data() - predicted_data

# visualize the residuals
fig, ax = plt.subplots()
ax.plot(residuals.flatten(), '*')
ax.set(
title='Residuals of fitted VAR model',
ylabel='Magnitude'
)

# compute the covariance of the residuals
model_order = conn.attrs.get('model_order')
t = residuals.shape[0]
sampled_residuals = np.concatenate(
np.split(residuals[:, :, model_order:], t, 0),
axis=2
).squeeze(0)
rescov = np.cov(sampled_residuals)

# Next, we visualize the covariance of residuals as before.
# Here we will see a similar trend with the covariances as
# with the covariances for time-varying VAR model.
fig, ax = plt.subplots()
cax = fig.add_axes([0.27, 0.8, 0.5, 0.05])
im = ax.imshow(rescov, cmap='viridis', aspect='equal', interpolation='none')
fig.colorbar(im, cax=cax, orientation='horizontal')
Binary file removed mne_connectivity/.DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions mne_connectivity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
from .envelope import envelope_correlation
from .io import read_connectivity
from .spectral import spectral_connectivity
from .var import vector_auto_regression
from .utils import check_indices, degree, seed_target_indices
Loading

0 comments on commit 373797a

Please sign in to comment.