Mismatch between MEG sensor-space and source-space cluster visualization

Hello everyone,

I am analyzing MEG time-frequency data and I am trying to understand a mismatch between sensor-space and source-space group statistics.

In sensor space, I observe positive significant clusters over frontal sensors for some contrasts and frequency bands. For example, for the contrast Condition A - Condition B, I see positive frontal sensor-level clusters in the alpha and beta bands.
However, in source space, the significant clusters are consistently visualized as negative/blue clusters, and I do not see corresponding positive/red clusters in alpha and beta bands. As an example, I am attaching images of comparisons, questions to the first image.

I would like to understand whether this reflects a real difference between sensor-level and source-level statistics, or whether something in my source-space plotting/statistics pipeline may be wrong.

For sensor-space analysis, the pipeline is:

  1. Load preprocessed MEG data and keep gradiometers only.
  2. Create condition-specific epochs from event markers. The epochs used for the CSD analysis are correct-response epochs.
  3. Compute CSDs in sensor space using Morlet wavelets:
csd = mne.time_frequency.csd_morlet(
    epochs,
    freqs,
    tmin=tmin,
    tmax=tmax,
    n_cycles=n_cycles,
    decim=decim,
    n_jobs=n_jobs,
)


  1. For each subject and contrast, compute condition-specific CSDs in the ROI time window and a paired baseline CSD time window.
    For example, for CoVi - SiVi:
A = CoVi Storage2 CSD
B = SiVi Storage2 CSD
baseline = CoVi_SiVi Storage1 paired-baseline CSD


The individual normalized sensor contrast is:

normalized_csd = (A - B) / paired_baseline


  1. For group-level sensor statistics, the already-computed individual normalized CSD contrast files are loaded:
S1_CoVi_vs_SiVi_normalized_csd.npy
S2_CoVi_vs_SiVi_normalized_csd.npy
...


Each file has shape:

channels × frequencies


The subject-level contrast arrays are stacked and transposed before statistics:

p = list_norm_csd  # subjects × channels × frequencies
obj = np.transpose(p, (0, 2, 1))  # subjects × frequencies × channels


Sensor adjacency is computed from a representative epochs object using gradiometers:

adj, ch_names = mne.channels.find_ch_adjacency(info, ch_type="grad")


Group-level cluster statistics are performed as a one-sample test against zero:

T_obs, clusters, cluster_p_values, H0 = mne.stats.spatio_temporal_cluster_1samp_test(
    obj,
    out_type=out_type,
    adjacency=adj,
    n_permutations=n_permutations,
    threshold=t_threshold,
    tail=tail,
)


So, at the sensor level, the group analysis does not subtract condition B from condition A again. It tests whether the individual normalized CSD contrasts differ from zero across subjects.

For the group-level source-space analysis, I use morphed source estimates that were already computed for each subject.

At the group level, I load three morphed STC files per subject:

stc_s  # condition A
stc_t  # condition B
stc_a  # paired baseline

For example, for CoVi_vs_SiVi, the intended contrast is:

CoVi - SiVi

The normalized source contrast is computed as:

group_1 = np.array([stc.data for stc in stc_s])
group_2 = np.array([stc.data for stc in stc_t])
group_a = np.array([stc.data for stc in stc_a])

diff = (group_1 - group_2) / group_a
STAT = np.transpose(diff, [0, 2, 1])  # subjects × time/frequency × sources

Then I run a one-sample source-space cluster permutation test against zero:

adjacency = mne.spatial_src_adjacency(src_fs)

T_obs, clusters, cluster_p_values, H0 = spatio_temporal_cluster_1samp_test(
    STAT,
    adjacency=adjacency,
    n_jobs=n_jobs,
    threshold=t_threshold,
    buffer_size=buffer_size,
    verbose=True,
    n_permutations=n_permutations,
    out_type=out_type,
)

For the final figures shown here, I use a signed T-value visualization of all significant clusters, not the MNE cluster-extent summary.

The function keeps T-values only inside significant clusters:

T_obs_sig = np.zeros_like(T_obs)

for idx in good_cluster_inds:
    cluster_mask = _cluster_to_mask(clusters[idx], T_obs.shape)
    T_obs_sig[cluster_mask] = T_obs[cluster_mask]

If T_obs has more than one time/frequency point, I collapse it into one source map by selecting, for each vertex, the signed T-value with the maximum absolute magnitude:

best_time = np.argmax(np.abs(T_obs_sig), axis=0)
signed_map = T_obs_sig[best_time, np.arange(T_obs_sig.shape[1])]
data = signed_map[:, np.newaxis]

Then I create a SourceEstimate and plot it using:

stc_signed.plot(
    subject="fsaverage",
    subjects_dir=subjects_dir,
    hemi=hemi,
    surface=surface,
    views=views,
    colormap="RdBu_r",
    clim=dict(kind="value", pos_lims=[absmax * 0.4, absmax * 0.7, absmax]),
    smoothing_steps=10,
    transparent=True,
    time_label="signed T-values",
    time_viewer=False,
    show_traces=False,
    background="white",
    foreground="black",
)

Given this pipeline, my main questions are:

  1. Is it expected that a positive frontal cluster in MEG sensor space does not necessarily appear as a positive frontal cluster in source space?

  2. Does the source-space statistical pipeline look conceptually correct: loading morphed STCs for condition A, condition B, and paired baseline, computing (A - B) / baseline, and then running a one-sample cluster permutation test against zero?

  3. Is my signed T-value visualization of source clusters appropriate, especially the step where I collapse multiple frequency/time points into one source map by selecting the signed T-value with the maximum absolute magnitude at each vertex?

  4. Could my plotting settings, especially RdBu_r, clim=dict(kind="value", pos_lims=[...]), smoothing, or transparency, cause positive/red clusters to be hidden or underrepresented?

In short, I would like to know whether the absence of positive/red frontal clusters in source space likely reflects the actual source-space statistics, or whether it may be caused by an issue in the source-space visualization/statistics code.

I am running the analysis in Jupyter Notebook with a Python [conda env:mne] kernel.
MNE version: 1.11.0

All sensor-level blue clusters seem to line up nicely with the source-level blue clusters. This would suggest your analysis pipeline is correct and also your way of visualizing the t-maps is appropriate. So why aren’t we seeing the red clusters? :thinking:

To rule out a plotting mistake, you could inspect the the numerical output of the cluster permutation test. It will give a list of all clusters found, associated p-values, and the sourcepoint+timepoints belonging to the cluster. That would tell us if the red clusters are actually found, but somehow not shown.

What if you flip around the comparison and do SiVe - CoVe? Then blue becomes red and red becomes blue. That could be a way to check for some sort of colormapping problem.

If the problem is more fundamental then we need to look into what kind of source could produce the red clusters on the sensor level. Those clusters are pretty large and uniform, indicating a very deep source. At this stage, perhaps it’s better to look at the actual source power maps instead of tmaps.