Intro to ConPy: functional connectivity estimation of MEG signals

Welcome to this introductory tutorial for the ConPy package. Together with MNE-Python, we can use it to perform functional connectivity estimation of MEG data. This tutorial was written to be used as an exercise during my lectures. In lieu of my lecture, you can read this paper to get the theoretical background you need to understand the concepts we will be dealing with in this exercise.

Ok, let's get started! I have similated some data for you. It's already stored on the virtual server you are talking to right now. In this simulation, a placed a couple of dipole sources on the cortex, sending out a signal in a narrow frequency band. During the first part of the recording, these sources are incoherent with each other. During the second part, some of the sources become coherent with each other. Your task is to find out:

  1. At which frequency are the sources sending out a signal?
  2. How many dipole sources did I place on the cortex?
  3. Where are the sources located on the cortex?
  4. Which sources are coherent in the second part of the recording?

We will use MNE-Python and ConPy to aswer the above questions.

Loading the required Python modules and configuring the environment

Executing the code cell below will load the required Python modules and configure some things. If all goes well, you'll be rewarded with a plot of a brain:


In [ ]:
# Don't worry about warnings in this exercise, as they can be distracting.
import warnings
warnings.simplefilter('ignore')

# Import the required Python modules
import mne
import conpy
import surfer

# Import and configure the 3D graphics backend
from mayavi import mlab
mlab.init_notebook('png')

# Tell MNE-Python to be quiet. The normal barrage of information will only distract us. Only display errors.
mne.set_log_level('ERROR')

# Configure the plotting interface to display plots in their own windows.
# The % sign makes it a "magic" command: a command ment for the notebook environment, rather than a command for Python.
%matplotlib notebook

# Tell MNE-Python and PySurfer where to find the brain model
import os
os.environ['SUBJECTS_DIR'] = 'data/subjects'

# Let's test plotting a brain (this is handled by the PySurfer package)
surfer.Brain('sample', hemi='both', surf='pial')

Loading the simulation data

If the code in the above cell ran without any errors, we're good to go. Let's load the stimulation data. It is stored as an MNE-Python Epochs file. To load it, you must use the mne.read_epochs function. To see how it works, you need to take a look at the documentation for this function. You can call up the documentation of any function by appending a ? to the function name, like so:


In [ ]:
mne.read_epochs?

The documentation shows us that mne.read_epochs takes one required parameter (fname) and three optional parameters (proj, preload and verbose). You can recognize optional parameters by the fact that they have a default value assigned to them. In this exercise, you can always leave the optional parameters as they are, unless explicitly instructed to change them.

So, the only parameter of interest right now is the fname parameter, which must be a string containing the path and filename of the simulated data, namely: 'data/simulated-data-epo.fif'. Go ahead and call the mne.read_epochs function to load the stimulated data. Store the result in a variable called epochs:


In [ ]:
# Write your Python code here

In [ ]:
# If your code in the above cell is correct, executing this cell print some information about the data
print(epochs)

"Epochs" are snippets of MEG sensor data. In this simulation, all sensors are gradiometers. There are two epochs, appoximately 10 second in length: one epoch corresponding to the (simulated) subject "at rest" and one epoch corresponding to the subject performing some task.

Most objects we'll be working with today have a plot method. For example, the cell below will plot the epochs object:


In [ ]:
# The semicolon at the end prevents the image from being included in this notebook
epochs.plot();

In the epochs plot, you can use the scrolling function of your mouse/trackpad to browse through the channels. The vertical dashed line indicates where one epoch ends and the next one begins.

Question 1: At which frequency are the sources sending out a signal?

To find out, let's plot the power spectal density (PSD) of the signal. The PSD is computed by applying a Fourier transform to the data of each MEG sensor. We can use the plot_psd method of the epochs object to show it to us. By default, it will show us the average PSD across the sensors, which is good enough for our purposes. Check the documentation of the epochs.plot_psd method to see what parameters are required (remember: you are free to ignore the optional parameters).


In [ ]:
# Write here the code to plot the PSD of the MEG signal

If you were to name one frequency at which the sources are sending out a signal, what would that frequency be? Fill in the answer below. We'll use it in the upcoming tasks:


In [ ]:
# Fill in the source frequency, in Hertz
source_frequency = ###

Question 2: How many sources did I simulate?

Ok, so now we know the frequency at which to look for sources. How many dipole sources did I use in the simulation? To find out, we must look at which sensors have the most activity at the frequency of the sources. The plot_psd_topomap method of the epochs object can do that for us. If you call it with the default parameters, it will plot so called "topomaps" for the following frequency bands:

Name Frequency band
Delta 0-4 Hz
Theta 4-8 Hz
Alpha 8-12 Hz
Beta 12-30 Hz
Gamma 30-45 Hz

Try it now: take a look at the documentation for the epochs.plot_psd_topomap method and plot some topomaps:


In [ ]:
# Write here the code to plot some PSD topomaps

Take a look at the topomap corresponding to the frequency band that contains the frequency at which the sources are sending out their signal. How many sources do you think I simulated? Fill in your answer below:


In [ ]:
number_of_sources = ###

Question 3: Where are the sources located on the cortex?

Looking at the topomaps will give you a rough location of the sources, but let's be more exact. We will now use a DICS beamformer to localize the sources on the cortex.

To construct a DICS beamformer, we must first estimate the cross-spectral density (CSD) between all sensors. You can use the mne.time_frequency.csd_morlet function to do so. Go check its documentation.

You will find that one of the parameters is a list of frequencies at which to compute the CSD. Use a list containing a single frequency: the answer to Question 1 that you stored earlier in the source_frequency variable. In Python code, the list can be written like this: [source_frequency]. Store the result of mne.time_frequency.csd_morlet in a variable called csd.


In [ ]:
# Write here the code to construct a CSD matrix

In [ ]:
# If the code in the cell above is correct, executing this cell will plot the CSD matrix
csd.plot()[0]

If you examine the CSD matrix closely, you can already spot which sources are coherent with each other. Sssshhh! we'll look at it in more detail later. For now, let's compute the DICS beamformer!

The next functions to call are mne.beamformer.make_dics and mne.beamformer.apply_dics_csd. Lets examine them more closely.

mne.beamformer.make_dics

This function will create the DICS beamformer weights. These weights are spatial filters: each filter will only pass activity for one specific location on the cortex, at one specific frequency(-band). In order to do this, we'll need a leadfield: a model that simulates how signals on the cortex manifest as magnetic fields as measured by the sensors. MNE-Python calls them "forward solutions". Luckily we have one lying around: the 'data/simulated-data-fwd.fif' file contains one. You can load it with the mne.read_forward_solution function. Take a look at the documentation for that function and load the forward solution in the variable fwd:


In [ ]:
# Write your code to read the forward solution here

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the source grid
fwd['src'].plot(trans='data/simulated-data-trans.fif')

For this exercise, we use a very sparse source grid (the yellow dots in the plot). This grid is enough for our purposes and our computations will run quickly. For real studies, I recommend a much denser grid.

Another thing you'll need for the DICS beamformer is an Info object. This object contains information about the location of the MEG sensors and so forth. The epochs object provides one as epochs.info. Try running print(epochs.info) to check it out.

Now you should have everything you need to create the DICS beamformer weights using the mne.beamformer.make_dics function. Store the result in the variable filters:


In [ ]:
# Write your code to compute the DICS filters here

In [ ]:
# If the code in the above cell is correct, executing this cell will print some information about the filters
print('Filters have been computed for %d points on the cortex at %d frequency.' %
      (filters['weights'].shape[1], filters['weights'].shape[0]))
print('At each point, there are %d source dipoles (XYZ)' % filters['n_orient'])

mne.beamformer.apply_dics_csd

With the DICS filters computed, making a cortical power map is straightforward. The mne.beamformer.apply_dics_csd will do it for you. The only new thing here is that this function will return two things (up to now, all our functions only returned one thing!). Don't panick. The Python syntax for dealing with it is like this:

power_map, frequencies = mne.beamformer.apply_dics_csd(...)

See? It returns both the power_map that we'll visualize in a minute, and a list of frequencies for which the power map is defined. Go read the documentation for mne.beamformer.apply_dics_csd and make the powermap:


In [ ]:
# Write your code to compute the power map here

In [ ]:
# If the code in the above cell is correct, executing the cell will plot the power map
power_map.plot(hemi='both', smoothing_steps=20);

Use the mouse/trackpad to rotate the brain around. Can you find the sources on the cortex? Even though I've simulated them as dipole sources, they show more as "blobs" in the power map. This is called spatial leaking and is due to various inaccuracies and limitations of the DICS beamformer filters.

Question 4: Which sources are coherent in the second part of the recording?

The simulated recording consists of two parts (=epochs): during the first epoch, our simulated subject is at rest and the sources are not coherent. During the second epoch, our simulated subject is performing a task that causes some of the sources to become coherent. It's finally time for some connectivity estimation!

We'll first tackle one-to-all connectivity, as it is much easier to visualize and the results are less messy. Afterward, we'll move on to all-to-all connectivity.

One-to-all connectivity estimation

For this, we must first define a "seed region": one of the source points for which we will estimate coherence with all other source points. A common choice is to use the power map to find the source point with the most power. To find this point, you can use the .argmax() method of the power_map.data object. This is a method that all data arrays have. It will return the index of the maximum element in the array, which in the case of our power_map.data array will be the source point with the maximum power.

Go find your seed point and store it in the variable seed_point:


In [ ]:
# Write your code to find the seed point here

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the seed point on the power map
brain = power_map.plot(hemi='both', smoothing_steps=20)  # Plot power map

# We need to find out on which hemisphere the seed point lies
lh_verts, rh_verts = power_map.vertices
if seed_point < len(lh_verts):
    # Seed point is on the left hemisphere
    brain.add_foci(lh_verts[seed_point], coords_as_verts=True, hemi='lh')
else:
    # Seed point is on the right hemisphere
    brain.add_foci(rh_verts[seed_point - len(lh_verts)], coords_as_verts=True, hemi='rh')

You may need to rotate the brain around to find the seed point. It should be drawn as a white sphere.

Up to now, we've been using all data. However, we know our sources are only coherent during the second part. Executing the cell below will split the data into a "rest" and "task" part.


In [ ]:
# Splitting the data is not hard to do.
epochs_rest = epochs['rest']
epochs_task = epochs['task']

To estimate connectivity for just the epochs_task part, we need to compute the CSD matrix on only this data. You've computed a CSD matrix before, so rince and repeat: compute the CSD on just the epochs_task data and store it in the csd_task variable:


In [ ]:
# Write your code here to compute the CSD on the epochs_task data

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the CSD matrix
csd_task.plot()[0]

Now you are ready to compute one-to-all connectivity using DICS. It will take two lines of Python code. First, you'll need to use the conpy.one_to_all_connectivity_pairs function to compute the list of connectivity pairs. Then, you can use the conpy.dics_connectivity function to perform the connectivity estimation. Check the documentation for both functions (remember: you can leave all optional parameters as they are) and store your connectivity result in the con_task variable:


In [ ]:
# Write your code here to compute one-to-all connectivity for the "task" data

In [ ]:
# If the code in the above cell is correct, executing this cell will print some information about the connectivity
print(con_task)

To visualize the connectivity result, we can create a cortical map, where the value at each source point is the coherence between the source point and the seed region. The con_task object defines a .make_stc() method that will do just that. Take a look at its documentation and store the map in the coherence_task variable:


In [ ]:
# Write your code here to compute the coherence map for the epochs_task data

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the coherence map
brain = coherence_task.plot(hemi='both', smoothing_steps=20)

lh_verts, rh_verts = coherence_task.vertices
if seed_point < len(lh_verts):
    # Seed point is on the left hemisphere
    brain.add_foci(lh_verts[seed_point], coords_as_verts=True, hemi='lh')
else:
    # Seed point is on the right hemisphere
    brain.add_foci(rh_verts[seed_point - len(lh_verts)], coords_as_verts=True, hemi='rh')

Which source points seem to be in coherence with the seed point? Double-click on the text-cell below to edit it and write down your answer.

Double-click here to edit this text cell. Pressing CTRL+Enter will transform it back into formatted text.

Congratulations! You have now answered the original 4 questions.

If you have some time left, you may continue below to explore all-to-all connectivity.

If you examine the coherence map, you'll find that the regions surrounding the seed point are coherent with the seed point. This is not because there are active coherent sources there, but because of the spatial leakage. You will always find this coherence. Make a one-to-all coherence map like you did above, but this time for the epochs_rest data (in which none of the sources are coherent). Store the connectivity in the con_rest variable and the coherence map in the coherence_rest variable:


In [ ]:
# Write your code here to compute connectivity for the epochs_rest data and make a coherence map

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the coherence map
brain = coherence_rest.plot(hemi='both', smoothing_steps=20)

lh_verts, rh_verts = coherence_rest.vertices
if seed_point < len(lh_verts):
    # Seed point is on the left hemisphere
    brain.add_foci(lh_verts[seed_point], coords_as_verts=True, hemi='lh')
else:
    # Seed point is on the right hemisphere
    brain.add_foci(rh_verts[seed_point - len(lh_verts)], coords_as_verts=True, hemi='rh')

See? You'll find that also when no coherent sources are active, there is an area of coherence surrounding the seed region. This will be a major problem when attempting to estimate all-to-all connectivity.

One way to deal with the spatial leakage problem is to make a contrast between the "task" and "rest" segments. Since the coherence due to spatial leakage is the same for both segments, it should cancel out.

Connectivity objects, like con_task and con_rest have support for common math operators like +, -, * and /. Creating a constract between the two object is therefore as simple as con_task - con_rest. Im the cell below, make a new coherence map of the contrast and store it in the coherence_contrast variable:


In [ ]:
# Write your code here to compute a contrast between the "task" and "rest" connectivity and make a coherence map

In [ ]:
# If the code in the above cell is correct, executing this cell will plot the coherence map
brain = coherence_contrast.plot(hemi='both', smoothing_steps=20)

lh_verts, rh_verts = coherence_contrast.vertices
if seed_point < len(lh_verts):
    # Seed point is on the left hemisphere
    brain.add_foci(lh_verts[seed_point], coords_as_verts=True, hemi='lh')
else:
    # Seed point is on the right hemisphere
    brain.add_foci(rh_verts[seed_point - len(lh_verts)], coords_as_verts=True, hemi='rh')

If all went well, you'll see that the coherence due to spatial leakage has disappeared from the coherence map.

All-to-all connectivity

Use the conpy.all_to_all_connectivity_pairs function to compute the connectivity pairs in an all-to-all manner. Then, use the conpy.dics_connectivity function like before to create a connectivity object for the "task" and a connectivity object for the "rest" data segments. Store them in the all_to_all_task and all_to_all_rest variables. You'll notice that computing all-to-all connectivity takes a while... Finally, create the contrast between them and store it in the all_to_all_contrast variable.


In [ ]:
# Write your code to produce all-to-all connectivity estimates for the "rest" and "task" segments
# and the contrast between them.

In [ ]:
# If the code in the above cell is correct, executing this cell will print some information about the connectivity
print(all_to_all_contrast)

How to visualize this all-to-all connectivity? This is a question worth pondering a bit. But for this exercise, we can get away with producing a coherence map like we did with the one-to-all connectivity. The value of the coherence map is, for each source point, the sum of the coherence of all connections from and to the source point.

Executing the cell below will plot this coherence map. Can you spot the connectivity between the sources?


In [ ]:
# This cell will plot the coherence map
all_to_all_coherence = all_to_all_contrast.make_stc()
all_to_all_coherence.plot(hemi='both', smoothing_steps=20);

––THE END––