blue blobs are for condition A != condition B

  • MNE version: 1.2.1
  • operating system: Ubuntu 20.04.05 LTS

Dear MNE community,

Iā€™m replicating the tutorial " 2 samples permutation test on source data with spatio-temporal clustering" with my own data, as in:

https://mne.tools/stable/auto_tutorials/stats-source-space/30_cluster_ftest_spatiotemporal.html#sphx-glr-auto-tutorials-stats-source-space-30-cluster-ftest-spatiotemporal-py

When I plot the results of my ā€˜spatio_temporal_cluster_testā€™ using ā€˜stc_all_cluster_vis.plotā€™ I get some unexpected issues.

I found 19 clusters and two of them are ā€œsignificantā€

In [55]: cluster_p_values
Out[55]: 
array([0.02, 0.98, 0.02, 0.98, 0.98, 1.  , 0.98, 0.98, 1.  , 0.98, 0.98,
       0.98, 0.98, 0.98, 0.98, 0.98, 0.72, 0.98, 0.98])
In [56]: stc_all_cluster_vis

Out[56]: <SourceEstimate | 20484 vertices, subject : fsaverage, tmin : 0.0 (ms), tmax : 200.0 (ms), tstep : 100.0 (ms), data shape : (20484, 3), ~640 kB>

However, when I plot the results, the spots representing the activations are quite sparsely distributed across the whole brain. I was expecting to find the activations, more concentrated in less areas, just like in the tutorial. If I have 19 clusters, shouldnā€™t I find 19 spots in the brain, instead of several?

In the tutorial itā€™s written that ā€œblue blobs are for condition A != condition Bā€. In my case, I have a blue and an orange blob:

Does the orange blob also represents that condition A != condition B?

If I alter my cluster_p_values variable by changing one of its entries to a value lower than 0.05, e.g:

In [59]: cluster_p_values[17] = 0.02

In [60]: cluster_p_values
Out[60]: 
array([0.02, 0.98, 0.02, 0.98, 0.98, 1.  , 0.98, 0.98, 1.  , 0.98, 0.98,
       0.98, 0.98, 0.98, 0.98, 0.98, 0.72, 0.02, 0.98])

Then Iā€™d have three ā€œsignificant clustersā€, instead of two, as in:

In [64]: stc_all_cluster_vis.shape

Out[64]: (20484, 4)

But even if I do this, still only two blobs are shown, one Blue and one Orange. Shouldnā€™t I have more blobs being shown, if I have more significant clusters?

Is it normal to get this spreaded activations across the entire brain, or is it a problem with my data?

Are my Root Mean Square (RMS) values acceptable? May it be causing the problems above?

It would be really nice to get any help with these issues. I can provide you more information if needed. Any hint would be quite helpful

Thanks in advance,

Bruno

Hereā€™s a bit of my code:

p_threshold = 0.001
df = X.shape[0] - 1  # degrees of freedom for the test

f_threshold = stats.distributions.f.ppf(1. - p_threshold / 2.,
                                        X.shape[0] - 1, X2.shape[0] - 1)

#%%

n_permutations = 50  # Should be above 1000.

F_obs, clusters, cluster_p_values, H0 = clu = \
    spatio_temporal_cluster_test([X, X2],
                                 n_permutations=n_permutations,
                                 threshold=f_threshold,
                                 max_step=3,n_jobs=3,
                                 seed=49,
                                 buffer_size=None,
                                 adjacency=adjacency,
                                 verbose=True)

#%%

# Select the clusters that are statistically significant at p < 0.05

good_cluster_inds = np.where(cluster_p_values < 0.05)[0]

#%%

stc_all_cluster_vis = summarize_clusters_stc(clu,
                                             tstep=tstep,
                                             vertices=fsave_vertices,
                                             subject='fsaverage')

#%%

#    Let's actually plot the first "time point" in the SourceEstimate, which
#    shows all the clusters, weighted by duration

# blue blobs are for condition A != condition B
brain = stc_all_cluster_vis.plot(
    'fsaverage',
    hemi='both', 
    views='lateral', 
    subjects_dir=subjects_dir,
    time_label='temporal extent (ms)', 
    size=(800, 800),
    smoothing_steps=5, 
    clim='auto')

#%%
1 Like

Hi bruno,

It seems like a Morphing issue to me. Did you morph your subject data to fsaverge before performing the cluster test? What values i.e., array shape (vertices x times) do you get for your stc data?

best,
Dip

Hi @dasdiptyajit, and thanks for your feedback!

I did morph my 17 subjects data individually to fsaverage.

First I used apply_inverse on the Evoked objects, apply baseline and crop them, and them morph them to fsaverage.

When I use apply_inverse, I have the following array shapes, after baselining and croping:

In [136]: condition_alpha

Out[136]: <SourceEstimate | 8196 vertices, subject : p043_A, tmin : 199.99999999999994 (ms), tmax : 399.99999999999994 (ms), tstep : 2.0 (ms), data shape : (8196, 101), ~6.4 MB>

In [137]: condition_alpha.shape

Out[137]: (8196, 101)

Then when I morph the objects to fsaverage, I get these shapes:

In [134]: morph_mat

Out[134]:

<20484x8196 sparse matrix of type '<class 'numpy.float64'>'

with 46280 stored elements in Compressed Sparse Row format>

In [135]: morph_mat.shape

Out[135]: (20484, 8196)

My code looks looks like this (I hope I did it correctly):

# SOURCE ESTIMATION

# Download fsaverage files
fs_dir = fetch_fsaverage(verbose=True)
subjects_dir = op.dirname(fs_dir)

# The files live in:
subject = 'fsaverage'
trans = 'fsaverage'  # MNE has a built-in fsaverage transformation
src_fname = op.join(fs_dir, 'bem', 'fsaverage-ico-5-src.fif')
bem = op.join(fs_dir, 'bem', 'fsaverage-5120-5120-5120-bem-sol.fif')

#%%

# Create lists

epochs_PM_alpha = []

morph_alpha = []

# Load the data

print('1.  PREPARE FOR LOOP WITH SUBJECTS')
# Set this to the directory all of the orignial Log_files directories live in

studydir = "/home/.../Epoched"

# Get all the paths!

path_epochs = glob.glob("%s/epochs_PMC_Tfr_p[0-9][0-9][0-9]_[A,T,S]-epo.fif"%(studydir))

# Some parameters for apply_inverse

snr = 3.0
lambda2 = 1.0 / snr ** 2
method = "dSPM"  # use dSPM method (could also be MNE, sLORETA, or eLORETA)

tmin = 0
tmax = 0.2

# Loop across subjects

for dir in list(path_epochs):
    if session == 'A':
        epochs_Tfr = mne.read_epochs(dir, preload=True)
        epochs_Tfr.apply_baseline(baseline=(-0.2, 0))# HERE I BASELINE MY EPOCH OBJECTS
        evoked = epochs_Tfr.average()
        epochs_Tfr.crop(tmin, tmax)
        fname_inv = '%s/p0%s_%s-inv.fif'%(studydir, subj, session)# I CREATED THE INVERSE PREVIOUSLY AND SAVED IT IN A DIRECTORY
        inverse_operator = read_inverse_operator(fname_inv)
        sample_vertices = [s['vertno'] for s in inverse_operator['src']]
        # Let's average and compute inverse
        condition_alpha = apply_inverse(evoked, inverse_operator, lambda2, method)# HERE I APPLY INVERSE
        condition_alpha = condition_alpha.apply_baseline(baseline=(-0.2,0))# HERE I APPLY BASELINE TO MY STC OBJECT
        condition_alpha.crop(tmin, tmax)
        epochs_PM_alpha.append(condition_alpha.data)
        src = mne.read_source_spaces(src_fname)
        fsave_vertices = [s['vertno'] for s in src]
        morph_mat = mne.compute_source_morph(# HERE I MORPH THEM TO FSAVERAGE
            src=inverse_operator['src'], subject_to='fsaverage',
            spacing=fsave_vertices, subjects_dir=subjects_dir).morph_mat
        morph_alpha.append(morph_mat)# HERE I APPEND THE MORPHED OBJECTS TO A LIST
        
tstep = condition_alpha.tstep * 1000  # convert to milliseconds

If you need any information or the rest of the code, please just tell me and I send it to you right away :slight_smile:

Hi,

Couple of things:

  1. If your evoked data is already baseline corrected, I donā€™t see a real necessity to compute a separate epochs baseline and re-correct for the stc data.

  2. In your code, I donā€™t see that you have applied the morphed matrix to the stc data. May be some codes are missing? Plus, what is your morphed stc data shape look like? (it should be fsaverage vertices (20484) * times (101))

  3. Did you try to visualize stc data for a subject with a plot function? Does it make sense? Do you see any source activity in the first place?

If you really want me to have a look at your script, I would recommend you to post the full script.

best,

1 Like

Hi @dasdiptyajit and many thanks for your answer!

  1. I thought the same, but if I donā€™t baseline correct my stc I get the entire brain activated, as below. I did this because I read someone was doing this in the forum. Should I stop baselining stc?

  2. I thought I did apply the morphed matrix to stc data by doing this:

morph_mat = mne.compute_source_morph(
                src=inverse_operator['src'], subject_to='fsaverage',
                spacing=fsave_vertices, subjects_dir=subjects_dir).morph_mat

Am I doing it right? After, reshaping and taking the dot product, my stc data matrix looks like this:

In [185]: X.shape

Out[185]: (17, 101, 20484)

with 17 subjects, 101 time points and 20484 vertices

You can see my entire script below (after that you can also find the answer to you third question):
My code:

#%%

import os.path as op
from mne.datasets import fetch_fsaverage
import pathlib
from scipy import stats as stats
import numpy as np
import mne
import matplotlib.pyplot as plt
import glob
from mne.minimum_norm import apply_inverse, read_inverse_operator
from mne.stats import spatio_temporal_cluster_test

#%%
# SOURCE ESTIMATION

# =============================================================================
# Compute the forward operator from EEG data using the standard template MRI subject fsaverage.
# =============================================================================

# Download fsaverage files
fs_dir = fetch_fsaverage(verbose=True)
subjects_dir = op.dirname(fs_dir)

# The files live in:
subject = 'fsaverage'
trans = 'fsaverage'  # MNE has a built-in fsaverage transformation
src_fname = op.join(fs_dir, 'bem', 'fsaverage-ico-5-src.fif')
bem = op.join(fs_dir, 'bem', 'fsaverage-5120-5120-5120-bem-sol.fif')

#%%

# Load the data

epochs_PM_alpha = []
epochs_PM_theta = []
epochs_PM_sham  = []

morph_alpha = []
morph_theta = []
morph_sham  = []

exclude_outliers = ['10',
                    '19',
                    '20',
                    '23',
                    '24',
                    '36',
                    '56',
                    '58',
                    '60']

excluded_subjs = []

# PM ERPs

print('1.  PREPARE FOR LOOP WITH SUBJECTS')
# Set this to the directory all of the orignial Log_files directories live in

studydir = "/home/.../Epoched"

# Get all the paths!
path_epochs = glob.glob("%s/epochs_PMC_Tfr_p[0-9][0-9][0-9]_[A,T,S]-epo.fif"%(studydir))#subdirs

snr = 3.0
lambda2 = 1.0 / snr ** 2
method = "dSPM"  # use dSPM method (could also be MNE, sLORETA, or eLORETA)

tmin = 0.2
tmax = 0.4

for dir in list(path_epochs):
    try:
        subj=(dir[-12:-10])
        session=(dir[-9])
        if subj in exclude_outliers:
            print('This subject will not be included!')
            excluded_subjs.append(str(subj))
            continue
        if session == 'A':
            epochs_Tfr = mne.read_epochs(dir, preload=True)
            epochs_Tfr.apply_baseline(baseline=(-0.2, 0))
            #epochs_Tfr.decimate(4)
            evoked = epochs_Tfr.average()
            epochs_Tfr.crop(tmin, tmax)
            fname_inv = '%s/p0%s_%s-inv.fif'%(studydir, subj, session)#subdirs''
            inverse_operator = read_inverse_operator(fname_inv)
            sample_vertices = [s['vertno'] for s in inverse_operator['src']]
            # Let's average and compute inverse
            condition_alpha = apply_inverse(evoked, inverse_operator, lambda2, method)
            condition_alpha = condition_alpha.apply_baseline(baseline=(-0.2,0))
            condition_alpha.crop(tmin, tmax)
            epochs_PM_alpha.append(condition_alpha.data)
            src = mne.read_source_spaces(src_fname)
            fsave_vertices = [s['vertno'] for s in src]
            morph_mat = mne.compute_source_morph(
                src=inverse_operator['src'], subject_to='fsaverage',
                spacing=fsave_vertices, subjects_dir=subjects_dir).morph_mat
            morph_alpha.append(morph_mat)
        elif session == 'T':
            epochs_Tfr = mne.read_epochs(dir, preload=True)
            epochs_Tfr.apply_baseline(baseline=(-0.2, 0))
            #epochs_Tfr.decimate(4)
            evoked = epochs_Tfr.average()
            epochs_Tfr.crop(tmin, tmax)
            fname_inv = '%s/p0%s_%s-inv.fif'%(studydir, subj, session)#subdirs''
            inverse_operator = read_inverse_operator(fname_inv)
            sample_vertices = [s['vertno'] for s in inverse_operator['src']]
            # Let's average and compute inverse
            condition_theta = apply_inverse(evoked, inverse_operator, lambda2, method)
            condition_theta = condition_theta.apply_baseline(baseline=(-0.2,0))
            condition_theta.crop(tmin, tmax)
            epochs_PM_theta.append(condition_theta.data)
            src = mne.read_source_spaces(src_fname)
            fsave_vertices = [s['vertno'] for s in src]
            morph_mat = mne.compute_source_morph(
                src=inverse_operator['src'], subject_to='fsaverage',
                spacing=fsave_vertices, subjects_dir=subjects_dir).morph_mat
            morph_theta.append(morph_mat)
        else:
            epochs_Tfr = mne.read_epochs(dir, preload=True)
            epochs_Tfr.apply_baseline(baseline=(-0.2, 0))
            #epochs_Tfr.decimate(4)
            evoked = epochs_Tfr.average()
            epochs_Tfr.crop(tmin, tmax)
            fname_inv = '%s/p0%s_%s-inv.fif'%(studydir, subj, session)#subdirs''
            inverse_operator = read_inverse_operator(fname_inv)
            sample_vertices = [s['vertno'] for s in inverse_operator['src']]
            # Let's average and compute inverse
            condition_sham = apply_inverse(evoked, inverse_operator, lambda2, method)
            condition_sham = condition_sham.apply_baseline(baseline=(-0.2,0))
            condition_sham.crop(tmin, tmax)
            epochs_PM_sham.append(condition_sham.data)
            src = mne.read_source_spaces(src_fname)
            fsave_vertices = [s['vertno'] for s in src]
            morph_mat = mne.compute_source_morph(
                src=inverse_operator['src'], subject_to='fsaverage',
                spacing=fsave_vertices, subjects_dir=subjects_dir).morph_mat
            morph_sham.append(morph_mat)
    except:
        print('Repeat manually: p%s_%s \n'%(subj,session))
    pass

tstep = condition_alpha.tstep * 1000  # convert to milliseconds

#%%

# Save arrays

np.save('/home/.../ERP_Source_alpha.npy', epochs_PM_alpha)
np.save('/home/.../ERP_Source_theta.npy', epochs_PM_theta)
np.save('/home/.../ERP_Source_sham.npy', epochs_PM_sham)

#%%

np.save('/home/.../ERP_Morph_alpha.npy', morph_alpha)
np.save('/home/.../ERP_Morph_theta.npy', morph_theta)
np.save('/home/.../ERP_Morph_sham.npy', morph_sham)

#%%

# Load arrays

X_alpha = np.load('/home/.../ERP_Source_alpha.npy', allow_pickle=True)
#X_theta = np.load('/home/.../ERP_Source_theta.npy', allow_pickle=True)
X_sham = np.load('/home/.../ERP_Source_sham.npy', allow_pickle=True)

#%%

morph_alpha = np.load('/home/.../ERP_Morph_alpha.npy', allow_pickle=True)
#morph_theta = np.load('/home/.../ERP_Morph_theta.npy', allow_pickle=True)
morph_sham = np.load('/home/.../ERP_Morph_sham.npy', allow_pickle=True)

#%%

n_vertices_fsave = morph_alpha[0].shape[0]
n_vertices_sample, n_times = X_alpha[0].data.shape
n_subjects = X_alpha.shape[0]

#%%

from numpy.random import randn
# Let's make sure our results replicate, so set the seed.
np.random.seed(0)
X = randn(n_subjects, n_vertices_sample, n_times)# * 10

#%%

# HERE I SUBTRACT X FROM X, SO I HAVE A MATRIX OF ZEROES AND SO I CAN SUBSEQUENTLY INSERT MY OWN DATA IN IT.

X = X-X
#X1 = X-X
X2 = X-X


#%%

X[:, :, :] += X_alpha[:, :, :]
#X1[:, :, :] += X_theta[:, :, :]
X2[:, :, :] += X_sham[:, :, :]

#%%

# We have to change the shape for the dot() to work properly
X = X.reshape(n_vertices_sample, n_times * n_subjects)
#X1 = X1.reshape(n_vertices_sample, n_times * n_subjects)
X2 = X2.reshape(n_vertices_sample, n_times * n_subjects)
print('Morphing data.')

#%%

del X_alpha
#del X_theta
del X_sham

#%%

X = morph_alpha[0].dot(X)  # morph_mat is a sparse matrix

#%%

#X1 = morph_theta[0].dot(X1)

#%%

X2 = morph_sham[0].dot(X2)

#%%

del morph_alpha
#del morph_theta
del morph_sham

#%%

X = X.reshape(n_vertices_fsave, n_times, n_subjects)
#X1 = X1.reshape(n_vertices_fsave, n_times, n_subjects)
X2 = X2.reshape(n_vertices_fsave, n_times, n_subjects)

#%%

# Find adjacency

fs_dir = fetch_fsaverage(verbose=True)
src_fname = op.join(fs_dir, 'bem', 'fsaverage-ico-5-src.fif')
src = mne.read_source_spaces(src_fname)


print('Computing adjacency.')
adjacency = mne.spatial_src_adjacency(src)

# mne.spatio_temporal_src_adjacency(src, n_times, dist=None, verbose=None)


#%%

# Compute statistic
# -----------------

# Note that X needs to be a multi-dimensional array of shape
# observations (subjects) Ɨ time Ɨ x frequency x space, so we permute dimensions
X = np.transpose(X, [2, 1, 0])
#X1 = np.transpose(X1, [2, 1, 0])
X2 = np.transpose(X2, [2, 1, 0])

#%%

del bem
del condition_alpha
del condition_theta
del condition_sham
del dann
del dir
del epochs_PM_alpha
del epochs_PM_theta
del epochs_PM_sham
del epochs_Tfr
del evoked
del exclude_outliers
del excluded_subjs
del fname_inv
del fs_dir
del inverse_operator
del lambda2
del method
del morph_mat
del path_epochs
del sample_vertices
del session
del snr
del src
del src_fname
del studydir
del subj
del subject
del trans


#%%

# Here we set a cluster forming threshold based on a p-value for
# the cluster based permutation test.
# We use a two-tailed threshold, the "1 - p_threshold" is needed
# because for two-tailed tests we must specify a positive threshold.
p_threshold = 0.001
df = X.shape[0] - 1  # degrees of freedom for the test

f_threshold = stats.distributions.f.ppf(1. - p_threshold / 2.,
                                        X.shape[0] - 1, X2.shape[0] - 1)

#f_threshold = dict(start=0., step=0.6)

#%%

import time
start = time.time()

n_permutations = 50  # Should be above 1000.

# Now let's actually do the clustering. This can take a long time...
print('Clustering.')
F_obs, clusters, cluster_p_values, H0 = clu = \
    spatio_temporal_cluster_test([X, X2],
                                 n_permutations=n_permutations,
                                 threshold=f_threshold,
                                 #tail=0,
                                 max_step=3,# Maximum distance between samples along the second axis of X to be considered adjacent (typically the second axis is the ā€œtimeā€ dimension)
                                 n_jobs=3,
                                 seed=49,
                                 buffer_size=None,
                                 adjacency=adjacency,
                                 #out_type='mask',# Maybe I'll have to change it to plot results properly
                                 verbose=True)

end = time.time()
time_elapsed = (end - start)
print(time_elapsed)
print(time_elapsed/60,' Minutes')
print(time_elapsed/60/60,' Hours')


#%%

# Select the clusters that are statistically significant at p < 0.05

good_cluster_inds = np.where(cluster_p_values < 0.05)[0]

#%%

print('Visualizing clusters.')

#    Now let's build a convenient representation of each cluster, where each
#    cluster becomes a "time point" in the SourceEstimate
#fsave_vertices = [np.arange(10242), np.arange(10242)]

#%%

stc_all_cluster_vis = summarize_clusters_stc(clu,
                                             tstep=tstep,
                                             vertices=fsave_vertices,
                                             subject='fsaverage')

#%%

#    Let's actually plot the first "time point" in the SourceEstimate, which
#    shows all the clusters, weighted by duration

# blue blobs are for condition A != condition B
brain = stc_all_cluster_vis.plot(
    'fsaverage',
    hemi='both', 
    views='lateral', 
    subjects_dir=subjects_dir,
    time_label='temporal extent (ms)', 
    size=(800, 800),
    smoothing_steps=5, 
    clim='auto')#dict(kind='value', pos_lims=[0, 1, 40]))

# We could save this via the following:
# brain.save_image('clusters.png')

#%%
  1. If I visualize stc for one subject just after apply_inverse (before morphing), then I get good results:
brain = condition_alpha.plot(surface='inflated',#'pial', 'white' (matter)
                 hemi='both',
                 subjects_dir=subjects_dir,
                 time_viewer=True)#  title=None,


I guess it looks just fine, doesnā€™t it?

Best,

Bruno

You should apply the morph via morph.apply(). I have the impression youā€™re working with the morph matrices here, not with the morphed stc data as you should. (But itā€™s difficult for me to tell because your code snippet is so long and convoluted ā€“ not great for a quick inspection!)

See this example on how to morph:
https://mne.tools/stable/auto_examples/inverse/morph_surface_stc.html#sphx-glr-auto-examples-inverse-morph-surface-stc-py

1 Like

Thanks @richard, I didnā€™t know I have to use morph.apply( ) to apply changes.

Another problem was that I was using ā€˜Transposeā€™, instead of ā€˜Reshapeā€™