Skip to content

Bug in envelope_correlation #8649

@Avoide

Description

@Avoide

Hi everyone,

I think there is a bug in the function envelope_correlation() in the case of orthogonalize="pairwise".
In the original article Hipp et al, 2012 they describe the correlation as between the orthogonalized Y on X with X.
So cor(y_ort, x)
But in the code in line 119:
corr[li] = np.dot(label_data_orth, data_mag_nomean[li])
The code seems to calculate the orthogonalized Y on all possible X with Y.
Say we have 5 channels
Then it should be:

  • C1_ort on C2 with C2
  • C1_ort on C3 with C3
  • C1_orth on C4 with C4
  • etc
    But what is being calculated in envelope_correlation() is C1_ort on C2 with C1.

I hope this is not too confusing, but I tried implementing a simple example using for loops and some of the source code to make sure I knew what timeseries are being orthogonalized to which one.

The code:

from mne.connectivity import envelope_correlation
from mne.filter import next_fast_len
import numpy as np
from scipy.stats import pearsonr
from scipy.signal import hilbert

n_epochs = 1
n_channels = 5
n_times = 100
data = np.random.rand(n_epochs, n_channels, n_times)
n_fft = next_fast_len(n_times)

data_hil = hilbert(data, N=n_fft, axis=-1)[..., :n_times]

corr = np.ones((n_epochs, n_channels, n_channels))

for epo_idx in range(data_hil.shape[0]):
    epoch_data = data_hil[epo_idx]
    data_mag = np.abs(epoch_data)
    data_conj_scaled = epoch_data.conj()
    data_conj_scaled /= data_mag
    for idx1 in range(data_hil.shape[1]):
        for idx2 in range(data_hil.shape[1]):
            # Get timeseries for each pair
            x = epoch_data[idx1]
            y = epoch_data[idx1]
            x_mag = np.abs(x)
            x_conj_scaled = epoch_data.conj()
            x_conj_scaled /= data_mag
            # Calculate orthogonalization
            y_orth_x = (y*x_conj_scaled[idx1]).imag
            # Estimate correlation
            cor = pearsonr(x_mag,y_orth_x)[0]
            corr[epo_idx,idx1,idx2] = cor

# Make it symmetric (it isn't at this point)
corr = np.abs(corr)
corr = (corr.T + corr) / 2.

ec = envelope_correlation(data_hil, combine=None, orthogonalize="pairwise")

print(np.allclose(corr, ec))

In addition to this I was also very confused with line 113 and 114:

# compute variances using linalg.norm (square, sum, sqrt) since mean=0
data_mag_std = np.linalg.norm(data_mag_nomean, axis=-1)

I tried using data_mag_std = np.std(data_mag, axis=-1) and got different results.
According to the documentation in numpy for np.std the operations should be (square, mean, sqrt) when mean = 0 and not sum. Using the sum you have to divide with N (and also sqrt afterwards)

std1 = np.std(data_mag, axis=-1)
std2 = np.linalg.norm(data_mag-np.mean(data_mag,axis=-1, keepdims=True),axis=-1)
print(np.allclose(std1,std2/np.sqrt(n_times)))

Fixing the std calculation and changing lines 119 and 120 to:

corr[li] = np.mean(label_data_orth*data_mag_nomean, axis=-1)
corr[li] /= data_mag_std

Seemed to fix the problem for me, but I didn't use the exact same implementation for my own data so it should probably be double-checked.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions